You've already forked DataMate
Revert "feat: fix the problem in the Operator Market frontend pages"
This commit is contained in:
@@ -1,332 +1,332 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Table, Badge, Button, Breadcrumb, Tooltip, App, Card, Input, Empty, Spin } from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import { mapFileData, mapKnowledgeBase } from "../knowledge-base.const";
|
||||
import {
|
||||
deleteKnowledgeBaseByIdUsingDelete,
|
||||
deleteKnowledgeBaseFileByIdUsingDelete,
|
||||
queryKnowledgeBaseByIdUsingGet,
|
||||
queryKnowledgeBaseFilesUsingGet,
|
||||
retrieveKnowledgeBaseContent,
|
||||
} from "../knowledge-base.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import AddDataDialog from "../components/AddDataDialog";
|
||||
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
|
||||
|
||||
interface StatisticItem {
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
interface RagChunk {
|
||||
id: string;
|
||||
text: string;
|
||||
metadata: string;
|
||||
}
|
||||
interface RecallResult {
|
||||
score: number;
|
||||
entity: RagChunk;
|
||||
id?: string | object;
|
||||
primaryKey?: string;
|
||||
}
|
||||
|
||||
const KnowledgeBaseDetailPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem | undefined>(undefined);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'fileList' | 'recallTest'>('fileList');
|
||||
const [recallLoading, setRecallLoading] = useState(false);
|
||||
const [recallResults, setRecallResults] = useState<RecallResult[]>([]);
|
||||
const [recallQuery, setRecallQuery] = useState("");
|
||||
|
||||
const fetchKnowledgeBaseDetails = async (id: string) => {
|
||||
const { data } = await queryKnowledgeBaseByIdUsingGet(id);
|
||||
setKnowledgeBase(mapKnowledgeBase(data));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchKnowledgeBaseDetails(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData: files,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData: fetchFiles,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<KBFile>(
|
||||
(params) => id ? queryKnowledgeBaseFilesUsingGet(id, params) : Promise.resolve({ data: [] }),
|
||||
mapFileData
|
||||
);
|
||||
|
||||
// File table logic
|
||||
const handleDeleteFile = async (file: KBFile) => {
|
||||
try {
|
||||
await deleteKnowledgeBaseFileByIdUsingDelete(knowledgeBase!.id, {
|
||||
ids: [file.id]
|
||||
});
|
||||
message.success("文件已删除");
|
||||
fetchFiles();
|
||||
} catch {
|
||||
message.error("文件删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库已删除");
|
||||
navigate("/data/knowledge-base");
|
||||
};
|
||||
|
||||
const handleRefreshPage = () => {
|
||||
if (knowledgeBase) {
|
||||
fetchKnowledgeBaseDetails(knowledgeBase.id);
|
||||
}
|
||||
fetchFiles();
|
||||
setShowEdit(false);
|
||||
};
|
||||
|
||||
const handleRecallTest = async () => {
|
||||
if (!recallQuery || !knowledgeBase?.id) return;
|
||||
setRecallLoading(true);
|
||||
try {
|
||||
const result = await retrieveKnowledgeBaseContent({
|
||||
query: recallQuery,
|
||||
topK: 10,
|
||||
threshold: 0.2,
|
||||
knowledgeBaseIds: [knowledgeBase.id],
|
||||
});
|
||||
setRecallResults(result?.data || []);
|
||||
} catch {
|
||||
setRecallResults([]);
|
||||
}
|
||||
setRecallLoading(false);
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑知识库",
|
||||
icon: <EditOutlined className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
setShowEdit(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "刷新知识库",
|
||||
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
handleRefreshPage();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除知识库",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该知识库吗?",
|
||||
description: "删除后将无法恢复,请谨慎操作。",
|
||||
cancelText: "取消",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
onConfirm: () => knowledgeBase && handleDeleteKB(knowledgeBase),
|
||||
},
|
||||
icon: <DeleteOutlined className="w-4 h-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
const fileOps = [
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除文件",
|
||||
icon: <DeleteOutlined className="w-4 h-4" />,
|
||||
danger: true,
|
||||
onClick: handleDeleteFile,
|
||||
},
|
||||
];
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
fixed: "left" as const,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "vectorizationStatus",
|
||||
width: 120,
|
||||
render: (status: unknown) => {
|
||||
if (typeof status === 'object' && status !== null) {
|
||||
const s = status as { color?: string; label?: string };
|
||||
return <Badge color={s.color} text={s.label} />;
|
||||
}
|
||||
return <Badge color="default" text={String(status)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "分块数",
|
||||
dataIndex: "chunkCount",
|
||||
key: "chunkCount",
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
align: "right" as const,
|
||||
width: 100,
|
||||
render: (_: unknown, file: KBFile) => (
|
||||
<div>
|
||||
{fileOps.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(file)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="mb-4">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<a onClick={() => navigate("/data/knowledge-base")}>知识库</a>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>{knowledgeBase?.name}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<DetailHeader
|
||||
data={knowledgeBase}
|
||||
statistics={knowledgeBase && Array.isArray((knowledgeBase as { statistics?: StatisticItem[] }).statistics)
|
||||
? ((knowledgeBase as { statistics?: StatisticItem[] }).statistics ?? [])
|
||||
: []}
|
||||
operations={operations}
|
||||
/>
|
||||
<CreateKnowledgeBase
|
||||
showBtn={false}
|
||||
isEdit={showEdit}
|
||||
data={knowledgeBase}
|
||||
onUpdate={handleRefreshPage}
|
||||
onClose={() => setShowEdit(false)}
|
||||
/>
|
||||
<div className="flex-1 border-card p-6 mt-4">
|
||||
<div className="flex items-center justify-between mb-4 gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type={activeTab === 'fileList' ? 'primary' : 'default'} onClick={() => setActiveTab('fileList')}>
|
||||
文件列表
|
||||
</Button>
|
||||
<Button type={activeTab === 'recallTest' ? 'primary' : 'default'} onClick={() => setActiveTab('recallTest')}>
|
||||
召回测试
|
||||
</Button>
|
||||
</div>
|
||||
{activeTab === 'fileList' && (
|
||||
<>
|
||||
<div className="flex-1">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索文件名..."
|
||||
filters={[]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: { type: [], status: [], tags: [] } })}
|
||||
showViewToggle={false}
|
||||
showReload={false}
|
||||
/>
|
||||
</div>
|
||||
<AddDataDialog knowledgeBase={knowledgeBase} onDataAdded={handleRefreshPage} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === 'fileList' ? (
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={fileColumns}
|
||||
dataSource={files}
|
||||
rowKey="id"
|
||||
pagination={pagination}
|
||||
scroll={{ y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2">
|
||||
<div style={{ fontSize: 14, fontWeight: 300, marginBottom: 8 }}>基于语义文本检索和全文检索后的加权平均结果</div>
|
||||
<div className="flex items-center mb-4">
|
||||
<Input.Search
|
||||
value={recallQuery}
|
||||
onChange={e => setRecallQuery(e.target.value)}
|
||||
onSearch={handleRecallTest}
|
||||
placeholder="请输入召回测试问题"
|
||||
enterButton="检索"
|
||||
loading={recallLoading}
|
||||
style={{ width: "100%", fontSize: 18, height: 48 }}
|
||||
/>
|
||||
</div>
|
||||
{recallLoading ? (
|
||||
<Spin className="mt-8" />
|
||||
) : recallResults.length === 0 ? (
|
||||
<Empty description="暂无召回结果" />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{recallResults.map((item, idx) => (
|
||||
<Card key={idx} title={`得分:${item.score?.toFixed(4) ?? "-"}`}
|
||||
extra={<span style={{ fontSize: 12 }}>ID: {item.entity?.id ?? "-"}</span>}
|
||||
style={{ wordBreak: "break-all" }}
|
||||
>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>{item.entity?.text ?? ""}</div>
|
||||
<div style={{ fontSize: 12, color: '#888' }}>
|
||||
metadata: <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>{item.entity?.metadata}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseDetailPage;
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Table, Badge, Button, Breadcrumb, Tooltip, App, Card, Input, Empty, Spin } from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import { mapFileData, mapKnowledgeBase } from "../knowledge-base.const";
|
||||
import {
|
||||
deleteKnowledgeBaseByIdUsingDelete,
|
||||
deleteKnowledgeBaseFileByIdUsingDelete,
|
||||
queryKnowledgeBaseByIdUsingGet,
|
||||
queryKnowledgeBaseFilesUsingGet,
|
||||
retrieveKnowledgeBaseContent,
|
||||
} from "../knowledge-base.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import AddDataDialog from "../components/AddDataDialog";
|
||||
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
|
||||
|
||||
interface StatisticItem {
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
interface RagChunk {
|
||||
id: string;
|
||||
text: string;
|
||||
metadata: string;
|
||||
}
|
||||
interface RecallResult {
|
||||
score: number;
|
||||
entity: RagChunk;
|
||||
id?: string | object;
|
||||
primaryKey?: string;
|
||||
}
|
||||
|
||||
const KnowledgeBaseDetailPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem | undefined>(undefined);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'fileList' | 'recallTest'>('fileList');
|
||||
const [recallLoading, setRecallLoading] = useState(false);
|
||||
const [recallResults, setRecallResults] = useState<RecallResult[]>([]);
|
||||
const [recallQuery, setRecallQuery] = useState("");
|
||||
|
||||
const fetchKnowledgeBaseDetails = async (id: string) => {
|
||||
const { data } = await queryKnowledgeBaseByIdUsingGet(id);
|
||||
setKnowledgeBase(mapKnowledgeBase(data));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchKnowledgeBaseDetails(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData: files,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData: fetchFiles,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<KBFile>(
|
||||
(params) => id ? queryKnowledgeBaseFilesUsingGet(id, params) : Promise.resolve({ data: [] }),
|
||||
mapFileData
|
||||
);
|
||||
|
||||
// File table logic
|
||||
const handleDeleteFile = async (file: KBFile) => {
|
||||
try {
|
||||
await deleteKnowledgeBaseFileByIdUsingDelete(knowledgeBase!.id, {
|
||||
ids: [file.id]
|
||||
});
|
||||
message.success("文件已删除");
|
||||
fetchFiles();
|
||||
} catch {
|
||||
message.error("文件删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库已删除");
|
||||
navigate("/data/knowledge-base");
|
||||
};
|
||||
|
||||
const handleRefreshPage = () => {
|
||||
if (knowledgeBase) {
|
||||
fetchKnowledgeBaseDetails(knowledgeBase.id);
|
||||
}
|
||||
fetchFiles();
|
||||
setShowEdit(false);
|
||||
};
|
||||
|
||||
const handleRecallTest = async () => {
|
||||
if (!recallQuery || !knowledgeBase?.id) return;
|
||||
setRecallLoading(true);
|
||||
try {
|
||||
const result = await retrieveKnowledgeBaseContent({
|
||||
query: recallQuery,
|
||||
topK: 10,
|
||||
threshold: 0.2,
|
||||
knowledgeBaseIds: [knowledgeBase.id],
|
||||
});
|
||||
setRecallResults(result?.data || []);
|
||||
} catch {
|
||||
setRecallResults([]);
|
||||
}
|
||||
setRecallLoading(false);
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑知识库",
|
||||
icon: <EditOutlined className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
setShowEdit(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "刷新知识库",
|
||||
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
handleRefreshPage();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除知识库",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该知识库吗?",
|
||||
description: "删除后将无法恢复,请谨慎操作。",
|
||||
cancelText: "取消",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
onConfirm: () => knowledgeBase && handleDeleteKB(knowledgeBase),
|
||||
},
|
||||
icon: <DeleteOutlined className="w-4 h-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
const fileOps = [
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除文件",
|
||||
icon: <DeleteOutlined className="w-4 h-4" />,
|
||||
danger: true,
|
||||
onClick: handleDeleteFile,
|
||||
},
|
||||
];
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
fixed: "left" as const,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "vectorizationStatus",
|
||||
width: 120,
|
||||
render: (status: unknown) => {
|
||||
if (typeof status === 'object' && status !== null) {
|
||||
const s = status as { color?: string; label?: string };
|
||||
return <Badge color={s.color} text={s.label} />;
|
||||
}
|
||||
return <Badge color="default" text={String(status)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "分块数",
|
||||
dataIndex: "chunkCount",
|
||||
key: "chunkCount",
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
align: "right" as const,
|
||||
width: 100,
|
||||
render: (_: unknown, file: KBFile) => (
|
||||
<div>
|
||||
{fileOps.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(file)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="mb-4">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<a onClick={() => navigate("/data/knowledge-base")}>知识库</a>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>{knowledgeBase?.name}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<DetailHeader
|
||||
data={knowledgeBase}
|
||||
statistics={knowledgeBase && Array.isArray((knowledgeBase as { statistics?: StatisticItem[] }).statistics)
|
||||
? ((knowledgeBase as { statistics?: StatisticItem[] }).statistics ?? [])
|
||||
: []}
|
||||
operations={operations}
|
||||
/>
|
||||
<CreateKnowledgeBase
|
||||
showBtn={false}
|
||||
isEdit={showEdit}
|
||||
data={knowledgeBase}
|
||||
onUpdate={handleRefreshPage}
|
||||
onClose={() => setShowEdit(false)}
|
||||
/>
|
||||
<div className="flex-1 border-card p-6 mt-4">
|
||||
<div className="flex items-center justify-between mb-4 gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type={activeTab === 'fileList' ? 'primary' : 'default'} onClick={() => setActiveTab('fileList')}>
|
||||
文件列表
|
||||
</Button>
|
||||
<Button type={activeTab === 'recallTest' ? 'primary' : 'default'} onClick={() => setActiveTab('recallTest')}>
|
||||
召回测试
|
||||
</Button>
|
||||
</div>
|
||||
{activeTab === 'fileList' && (
|
||||
<>
|
||||
<div className="flex-1">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索文件名..."
|
||||
filters={[]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: { type: [], status: [], tags: [] } })}
|
||||
showViewToggle={false}
|
||||
showReload={false}
|
||||
/>
|
||||
</div>
|
||||
<AddDataDialog knowledgeBase={knowledgeBase} onDataAdded={handleRefreshPage} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === 'fileList' ? (
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={fileColumns}
|
||||
dataSource={files}
|
||||
rowKey="id"
|
||||
pagination={pagination}
|
||||
scroll={{ y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2">
|
||||
<div style={{ fontSize: 14, fontWeight: 300, marginBottom: 8 }}>基于语义文本检索和全文检索后的加权平均结果</div>
|
||||
<div className="flex items-center mb-4">
|
||||
<Input.Search
|
||||
value={recallQuery}
|
||||
onChange={e => setRecallQuery(e.target.value)}
|
||||
onSearch={handleRecallTest}
|
||||
placeholder="请输入召回测试问题"
|
||||
enterButton="检索"
|
||||
loading={recallLoading}
|
||||
style={{ width: "100%", fontSize: 18, height: 48 }}
|
||||
/>
|
||||
</div>
|
||||
{recallLoading ? (
|
||||
<Spin className="mt-8" />
|
||||
) : recallResults.length === 0 ? (
|
||||
<Empty description="暂无召回结果" />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{recallResults.map((item, idx) => (
|
||||
<Card key={idx} title={`得分:${item.score?.toFixed(4) ?? "-"}`}
|
||||
extra={<span style={{ fontSize: 12 }}>ID: {item.entity?.id ?? "-"}</span>}
|
||||
style={{ wordBreak: "break-all" }}
|
||||
>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>{item.entity?.text ?? ""}</div>
|
||||
<div style={{ fontSize: 12, color: '#888' }}>
|
||||
metadata: <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>{item.entity?.metadata}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseDetailPage;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,180 +1,180 @@
|
||||
import { useState } from "react";
|
||||
import { Card, Button, Table, Tooltip, message } from "antd";
|
||||
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { useNavigate } from "react-router";
|
||||
import CardView from "@/components/CardView";
|
||||
import {
|
||||
deleteKnowledgeBaseByIdUsingDelete,
|
||||
queryKnowledgeBasesUsingPost,
|
||||
} from "../knowledge-base.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
|
||||
import { mapKnowledgeBase } from "../knowledge-base.const";
|
||||
|
||||
export default function KnowledgeBasePage() {
|
||||
const navigate = useNavigate();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [currentKB, setCurrentKB] = useState<KnowledgeBaseItem | null>(null);
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<KnowledgeBaseItem>(
|
||||
queryKnowledgeBasesUsingPost,
|
||||
(kb) => mapKnowledgeBase(kb, false) // 在首页不显示索引模型和文本理解模型字段
|
||||
);
|
||||
|
||||
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||
try {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库删除成功");
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
message.error("知识库删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item) => {
|
||||
setIsEdit(true);
|
||||
setCurrentKB(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
confirm: {
|
||||
title: "确认删除",
|
||||
description: "此操作不可撤销,是否继续?",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
},
|
||||
onClick: (item) => handleDeleteKB(item),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "知识库",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/knowledge-base/detail/${kb.id}`)}
|
||||
>
|
||||
{kb.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 150,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op.danger}
|
||||
onClick={() => op.onClick(kb)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
// Main list view
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">知识生成</h1>
|
||||
<CreateKnowledgeBase
|
||||
isEdit={isEdit}
|
||||
data={currentKB}
|
||||
onUpdate={() => {
|
||||
fetchData();
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsEdit(false);
|
||||
setCurrentKB(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索知识库..."
|
||||
filters={[]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={fetchData}
|
||||
/>
|
||||
{viewMode === "card" ? (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={operations}
|
||||
onView={(item) => navigate(`/data/knowledge-base/detail/${item.id}`)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
loading={loading}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 20rem)" }}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
import { useState } from "react";
|
||||
import { Card, Button, Table, Tooltip, message } from "antd";
|
||||
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { useNavigate } from "react-router";
|
||||
import CardView from "@/components/CardView";
|
||||
import {
|
||||
deleteKnowledgeBaseByIdUsingDelete,
|
||||
queryKnowledgeBasesUsingPost,
|
||||
} from "../knowledge-base.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
|
||||
import { mapKnowledgeBase } from "../knowledge-base.const";
|
||||
|
||||
export default function KnowledgeBasePage() {
|
||||
const navigate = useNavigate();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [currentKB, setCurrentKB] = useState<KnowledgeBaseItem | null>(null);
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<KnowledgeBaseItem>(
|
||||
queryKnowledgeBasesUsingPost,
|
||||
(kb) => mapKnowledgeBase(kb, false) // 在首页不显示索引模型和文本理解模型字段
|
||||
);
|
||||
|
||||
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||
try {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库删除成功");
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
message.error("知识库删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item) => {
|
||||
setIsEdit(true);
|
||||
setCurrentKB(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
confirm: {
|
||||
title: "确认删除",
|
||||
description: "此操作不可撤销,是否继续?",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
},
|
||||
onClick: (item) => handleDeleteKB(item),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "知识库",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/knowledge-base/detail/${kb.id}`)}
|
||||
>
|
||||
{kb.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 150,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op.danger}
|
||||
onClick={() => op.onClick(kb)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
// Main list view
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">知识生成</h1>
|
||||
<CreateKnowledgeBase
|
||||
isEdit={isEdit}
|
||||
data={currentKB}
|
||||
onUpdate={() => {
|
||||
fetchData();
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsEdit(false);
|
||||
setCurrentKB(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索知识库..."
|
||||
filters={[]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={fetchData}
|
||||
/>
|
||||
{viewMode === "card" ? (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={operations}
|
||||
onView={(item) => navigate(`/data/knowledge-base/detail/${item.id}`)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
loading={loading}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 20rem)" }}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,353 +1,353 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
App,
|
||||
Input,
|
||||
Select,
|
||||
Form,
|
||||
Modal,
|
||||
Steps,
|
||||
Descriptions,
|
||||
Table,
|
||||
} from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
||||
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
||||
import { DescriptionsItemType } from "antd/es/descriptions";
|
||||
import { DatasetFileCols } from "../knowledge-base.const";
|
||||
|
||||
const sliceOptions = [
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
{ label: "章节分块", value: "CHAPTER_CHUNK" },
|
||||
{ label: "段落分块", value: "PARAGRAPH_CHUNK" },
|
||||
{ label: "长度分块", value: "LENGTH_CHUNK" },
|
||||
{ label: "自定义分割符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||
];
|
||||
|
||||
export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
const [selectedFilesMap, setSelectedFilesMap] = useState({});
|
||||
|
||||
// 定义分块选项
|
||||
const sliceOptions = [
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
{ label: "按章节分块", value: "CHAPTER_CHUNK" },
|
||||
{ label: "按段落分块", value: "PARAGRAPH_CHUNK" },
|
||||
{ label: "固定长度分块", value: "FIXED_LENGTH_CHUNK" },
|
||||
{ label: "自定义分隔符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||
];
|
||||
|
||||
// 定义初始状态
|
||||
const [newKB, setNewKB] = useState({
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: "",
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: "选择数据集文件",
|
||||
description: "从多个数据集中选择文件",
|
||||
},
|
||||
{
|
||||
title: "配置参数",
|
||||
description: "设置数据处理参数",
|
||||
},
|
||||
{
|
||||
title: "确认上传",
|
||||
description: "确认信息并上传",
|
||||
},
|
||||
];
|
||||
|
||||
// 获取已选择文件总数
|
||||
const getSelectedFilesCount = () => {
|
||||
return Object.values(selectedFilesMap).reduce(
|
||||
(total, ids) => total + ids.length,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
// 验证当前步骤
|
||||
if (currentStep === 0) {
|
||||
if (getSelectedFilesCount() === 0) {
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currentStep === 1) {
|
||||
// 验证切片参数
|
||||
if (!newKB.processType) {
|
||||
message.warning("请选择分块方式");
|
||||
return;
|
||||
}
|
||||
if (!newKB.chunkSize || Number(newKB.chunkSize) <= 0) {
|
||||
message.warning("请输入有效的分块大小");
|
||||
return;
|
||||
}
|
||||
if (!newKB.overlapSize || Number(newKB.overlapSize) < 0) {
|
||||
message.warning("请输入有效的重叠长度");
|
||||
return;
|
||||
}
|
||||
if (newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && !newKB.delimiter) {
|
||||
message.warning("请输入分隔符");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setCurrentStep(currentStep + 1);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
setCurrentStep(currentStep - 1);
|
||||
};
|
||||
|
||||
// 重置所有状态
|
||||
const handleReset = () => {
|
||||
setCurrentStep(0);
|
||||
setNewKB({
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: "",
|
||||
});
|
||||
form.resetFields();
|
||||
setSelectedFilesMap({});
|
||||
};
|
||||
|
||||
const handleAddData = async () => {
|
||||
if (getSelectedFilesCount() === 0) {
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造符合API要求的请求数据
|
||||
const requestData = {
|
||||
files: Object.values(selectedFilesMap),
|
||||
processType: newKB.processType,
|
||||
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
||||
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
||||
delimiter: newKB.delimiter,
|
||||
};
|
||||
|
||||
await addKnowledgeBaseFilesUsingPost(knowledgeBase.id, requestData);
|
||||
|
||||
// 先通知父组件刷新数据(确保刷新发生在重置前)
|
||||
onDataAdded?.();
|
||||
|
||||
message.success("数据添加成功");
|
||||
// 重置状态
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
message.error("数据添加失败,请重试");
|
||||
console.error("添加文件失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const descItems: DescriptionsItemType[] = [
|
||||
{
|
||||
label: "知识库名称",
|
||||
key: "knowledgeBaseName",
|
||||
children: knowledgeBase?.name,
|
||||
},
|
||||
{
|
||||
label: "数据来源",
|
||||
key: "dataSource",
|
||||
children: "数据集",
|
||||
},
|
||||
{
|
||||
label: "文件总数",
|
||||
key: "totalFileCount",
|
||||
children: Object.keys(selectedFilesMap).length,
|
||||
},
|
||||
{
|
||||
label: "分块方式",
|
||||
key: "chunkingMethod",
|
||||
children:
|
||||
sliceOptions.find((opt) => opt.value === newKB.processType)?.label ||
|
||||
"",
|
||||
},
|
||||
{
|
||||
label: "分块大小",
|
||||
key: "chunkSize",
|
||||
children: newKB.chunkSize,
|
||||
},
|
||||
{
|
||||
label: "重叠长度",
|
||||
key: "overlapSize",
|
||||
children: newKB.overlapSize,
|
||||
},
|
||||
...(newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && newKB.delimiter
|
||||
? [
|
||||
{
|
||||
label: "分隔符",
|
||||
children: <span className="font-mono">{newKB.delimiter}</span>,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "文件列表",
|
||||
key: "fileList",
|
||||
span: 3,
|
||||
children: (
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={Object.values(selectedFilesMap)}
|
||||
columns={DatasetFileCols}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
handleReset();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
添加数据
|
||||
</Button>
|
||||
<Modal
|
||||
title="添加数据"
|
||||
open={open}
|
||||
onCancel={handleModalCancel}
|
||||
footer={
|
||||
<div className="space-x-2">
|
||||
{currentStep === 0 && (
|
||||
<Button onClick={handleModalCancel}>取消</Button>
|
||||
)}
|
||||
{currentStep > 0 && (
|
||||
<Button disabled={false} onClick={handlePrev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < steps.length - 1 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={
|
||||
Object.keys(selectedFilesMap).length === 0 ||
|
||||
!newKB.chunkSize ||
|
||||
!newKB.overlapSize ||
|
||||
!newKB.processType
|
||||
}
|
||||
onClick={handleNext}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" onClick={handleAddData}>
|
||||
确认上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width={1000}
|
||||
>
|
||||
<div>
|
||||
{/* 步骤导航 */}
|
||||
<Steps
|
||||
current={currentStep}
|
||||
size="small"
|
||||
items={steps}
|
||||
labelPlacement="vertical"
|
||||
/>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
{currentStep === 0 && (
|
||||
<DatasetFileTransfer
|
||||
open={open}
|
||||
selectedFilesMap={selectedFilesMap}
|
||||
onSelectedFilesChange={setSelectedFilesMap}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form
|
||||
hidden={currentStep !== 1}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={newKB}
|
||||
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<Form.Item
|
||||
label="分块方式"
|
||||
name="processType"
|
||||
required
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select options={sliceOptions} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Form.Item
|
||||
label="分块大小"
|
||||
name="chunkSize"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入分块大小",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" placeholder="请输入分块大小" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="重叠长度"
|
||||
name="overlapSize"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入重叠长度",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" placeholder="请输入重叠长度" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && (
|
||||
<Form.Item
|
||||
label="分隔符"
|
||||
name="delimiter"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入分隔符",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="输入分隔符,如 \n\n" />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div className="space-y-6" hidden={currentStep !== 2}>
|
||||
<div className="">
|
||||
<div className="text-lg font-medium mb-3">上传信息确认</div>
|
||||
<Descriptions layout="vertical" size="small" items={descItems} />
|
||||
</div>
|
||||
<div className="text-sm text-yellow-600">
|
||||
提示:上传后系统将自动处理文件,请耐心等待
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
App,
|
||||
Input,
|
||||
Select,
|
||||
Form,
|
||||
Modal,
|
||||
Steps,
|
||||
Descriptions,
|
||||
Table,
|
||||
} from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
||||
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
||||
import { DescriptionsItemType } from "antd/es/descriptions";
|
||||
import { DatasetFileCols } from "../knowledge-base.const";
|
||||
|
||||
const sliceOptions = [
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
{ label: "章节分块", value: "CHAPTER_CHUNK" },
|
||||
{ label: "段落分块", value: "PARAGRAPH_CHUNK" },
|
||||
{ label: "长度分块", value: "LENGTH_CHUNK" },
|
||||
{ label: "自定义分割符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||
];
|
||||
|
||||
export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
const [selectedFilesMap, setSelectedFilesMap] = useState({});
|
||||
|
||||
// 定义分块选项
|
||||
const sliceOptions = [
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
{ label: "按章节分块", value: "CHAPTER_CHUNK" },
|
||||
{ label: "按段落分块", value: "PARAGRAPH_CHUNK" },
|
||||
{ label: "固定长度分块", value: "FIXED_LENGTH_CHUNK" },
|
||||
{ label: "自定义分隔符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||
];
|
||||
|
||||
// 定义初始状态
|
||||
const [newKB, setNewKB] = useState({
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: "",
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: "选择数据集文件",
|
||||
description: "从多个数据集中选择文件",
|
||||
},
|
||||
{
|
||||
title: "配置参数",
|
||||
description: "设置数据处理参数",
|
||||
},
|
||||
{
|
||||
title: "确认上传",
|
||||
description: "确认信息并上传",
|
||||
},
|
||||
];
|
||||
|
||||
// 获取已选择文件总数
|
||||
const getSelectedFilesCount = () => {
|
||||
return Object.values(selectedFilesMap).reduce(
|
||||
(total, ids) => total + ids.length,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
// 验证当前步骤
|
||||
if (currentStep === 0) {
|
||||
if (getSelectedFilesCount() === 0) {
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currentStep === 1) {
|
||||
// 验证切片参数
|
||||
if (!newKB.processType) {
|
||||
message.warning("请选择分块方式");
|
||||
return;
|
||||
}
|
||||
if (!newKB.chunkSize || Number(newKB.chunkSize) <= 0) {
|
||||
message.warning("请输入有效的分块大小");
|
||||
return;
|
||||
}
|
||||
if (!newKB.overlapSize || Number(newKB.overlapSize) < 0) {
|
||||
message.warning("请输入有效的重叠长度");
|
||||
return;
|
||||
}
|
||||
if (newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && !newKB.delimiter) {
|
||||
message.warning("请输入分隔符");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setCurrentStep(currentStep + 1);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
setCurrentStep(currentStep - 1);
|
||||
};
|
||||
|
||||
// 重置所有状态
|
||||
const handleReset = () => {
|
||||
setCurrentStep(0);
|
||||
setNewKB({
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: "",
|
||||
});
|
||||
form.resetFields();
|
||||
setSelectedFilesMap({});
|
||||
};
|
||||
|
||||
const handleAddData = async () => {
|
||||
if (getSelectedFilesCount() === 0) {
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造符合API要求的请求数据
|
||||
const requestData = {
|
||||
files: Object.values(selectedFilesMap),
|
||||
processType: newKB.processType,
|
||||
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
||||
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
||||
delimiter: newKB.delimiter,
|
||||
};
|
||||
|
||||
await addKnowledgeBaseFilesUsingPost(knowledgeBase.id, requestData);
|
||||
|
||||
// 先通知父组件刷新数据(确保刷新发生在重置前)
|
||||
onDataAdded?.();
|
||||
|
||||
message.success("数据添加成功");
|
||||
// 重置状态
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
message.error("数据添加失败,请重试");
|
||||
console.error("添加文件失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const descItems: DescriptionsItemType[] = [
|
||||
{
|
||||
label: "知识库名称",
|
||||
key: "knowledgeBaseName",
|
||||
children: knowledgeBase?.name,
|
||||
},
|
||||
{
|
||||
label: "数据来源",
|
||||
key: "dataSource",
|
||||
children: "数据集",
|
||||
},
|
||||
{
|
||||
label: "文件总数",
|
||||
key: "totalFileCount",
|
||||
children: Object.keys(selectedFilesMap).length,
|
||||
},
|
||||
{
|
||||
label: "分块方式",
|
||||
key: "chunkingMethod",
|
||||
children:
|
||||
sliceOptions.find((opt) => opt.value === newKB.processType)?.label ||
|
||||
"",
|
||||
},
|
||||
{
|
||||
label: "分块大小",
|
||||
key: "chunkSize",
|
||||
children: newKB.chunkSize,
|
||||
},
|
||||
{
|
||||
label: "重叠长度",
|
||||
key: "overlapSize",
|
||||
children: newKB.overlapSize,
|
||||
},
|
||||
...(newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && newKB.delimiter
|
||||
? [
|
||||
{
|
||||
label: "分隔符",
|
||||
children: <span className="font-mono">{newKB.delimiter}</span>,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "文件列表",
|
||||
key: "fileList",
|
||||
span: 3,
|
||||
children: (
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={Object.values(selectedFilesMap)}
|
||||
columns={DatasetFileCols}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
handleReset();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
添加数据
|
||||
</Button>
|
||||
<Modal
|
||||
title="添加数据"
|
||||
open={open}
|
||||
onCancel={handleModalCancel}
|
||||
footer={
|
||||
<div className="space-x-2">
|
||||
{currentStep === 0 && (
|
||||
<Button onClick={handleModalCancel}>取消</Button>
|
||||
)}
|
||||
{currentStep > 0 && (
|
||||
<Button disabled={false} onClick={handlePrev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < steps.length - 1 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={
|
||||
Object.keys(selectedFilesMap).length === 0 ||
|
||||
!newKB.chunkSize ||
|
||||
!newKB.overlapSize ||
|
||||
!newKB.processType
|
||||
}
|
||||
onClick={handleNext}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" onClick={handleAddData}>
|
||||
确认上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width={1000}
|
||||
>
|
||||
<div>
|
||||
{/* 步骤导航 */}
|
||||
<Steps
|
||||
current={currentStep}
|
||||
size="small"
|
||||
items={steps}
|
||||
labelPlacement="vertical"
|
||||
/>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
{currentStep === 0 && (
|
||||
<DatasetFileTransfer
|
||||
open={open}
|
||||
selectedFilesMap={selectedFilesMap}
|
||||
onSelectedFilesChange={setSelectedFilesMap}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form
|
||||
hidden={currentStep !== 1}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={newKB}
|
||||
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<Form.Item
|
||||
label="分块方式"
|
||||
name="processType"
|
||||
required
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select options={sliceOptions} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Form.Item
|
||||
label="分块大小"
|
||||
name="chunkSize"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入分块大小",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" placeholder="请输入分块大小" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="重叠长度"
|
||||
name="overlapSize"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入重叠长度",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" placeholder="请输入重叠长度" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && (
|
||||
<Form.Item
|
||||
label="分隔符"
|
||||
name="delimiter"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入分隔符",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="输入分隔符,如 \n\n" />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div className="space-y-6" hidden={currentStep !== 2}>
|
||||
<div className="">
|
||||
<div className="text-lg font-medium mb-3">上传信息确认</div>
|
||||
<Descriptions layout="vertical" size="small" items={descItems} />
|
||||
</div>
|
||||
<div className="text-sm text-yellow-600">
|
||||
提示:上传后系统将自动处理文件,请耐心等待
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,178 +1,178 @@
|
||||
import { Button, Form, Input, message, Modal, Select } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis";
|
||||
import { ModelI } from "@/pages/SettingsPage/ModelAccess";
|
||||
import {
|
||||
createKnowledgeBaseUsingPost,
|
||||
updateKnowledgeBaseByIdUsingPut,
|
||||
} from "../knowledge-base.api";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import { showSettings } from "@/store/slices/settingsSlice";
|
||||
|
||||
export default function CreateKnowledgeBase({
|
||||
isEdit,
|
||||
data,
|
||||
showBtn = true,
|
||||
onUpdate,
|
||||
onClose,
|
||||
}: {
|
||||
isEdit?: boolean;
|
||||
showBtn?: boolean;
|
||||
data?: Partial<KnowledgeBaseItem> | null;
|
||||
onUpdate: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [models, setModels] = useState<ModelI[]>([]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const embeddingModelOptions = models
|
||||
.filter((model) => model.type === "EMBEDDING")
|
||||
.map((model) => ({
|
||||
label: model.modelName + " (" + model.provider + ")",
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const chatModelOptions = models
|
||||
.filter((model) => model.type === "CHAT")
|
||||
.map((model) => ({
|
||||
label: model.modelName + " (" + model.provider + ")",
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const fetchModels = async () => {
|
||||
const { data } = await queryModelListUsingGet({ page: 0, size: 1000 });
|
||||
setModels(data.content || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) fetchModels();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && data) {
|
||||
setOpen(true);
|
||||
form.setFieldsValue({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
embeddingModel: data.embeddingModel,
|
||||
chatModel: data.chatModel,
|
||||
});
|
||||
}
|
||||
}, [isEdit, data, form]);
|
||||
|
||||
const handleCreateKnowledgeBase = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (isEdit && data) {
|
||||
await updateKnowledgeBaseByIdUsingPut(data.id!, values);
|
||||
message.success("知识库更新成功");
|
||||
} else {
|
||||
await createKnowledgeBaseUsingPost(values);
|
||||
message.success("知识库创建成功");
|
||||
}
|
||||
setOpen(false);
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
message.error("操作失败:", error.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpen(false);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBtn && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
创建知识库
|
||||
</Button>
|
||||
)}
|
||||
<Modal
|
||||
title={isEdit ? "编辑知识库" : "创建知识库"}
|
||||
open={open}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
maskClosable={false}
|
||||
onCancel={handleCloseModal}
|
||||
onOk={handleCreateKnowledgeBase}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="知识库名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入知识库名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入知识库名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="描述"
|
||||
name="description"
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<Input.TextArea placeholder="请输入知识库描述(可选)" rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="索引模型"
|
||||
name="embeddingModel"
|
||||
rules={[{ required: true, message: "请选择索引模型" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择索引模型"
|
||||
options={embeddingModelOptions}
|
||||
disabled={isEdit} // 编辑模式下禁用索引模型修改
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
block
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(showSettings())}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="文本理解模型"
|
||||
name="chatModel"
|
||||
rules={[{ required: true, message: "请选择文本理解模型" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择文本理解模型"
|
||||
options={chatModelOptions}
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
block
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(showSettings())}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { Button, Form, Input, message, Modal, Select } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis";
|
||||
import { ModelI } from "@/pages/SettingsPage/ModelAccess";
|
||||
import {
|
||||
createKnowledgeBaseUsingPost,
|
||||
updateKnowledgeBaseByIdUsingPut,
|
||||
} from "../knowledge-base.api";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import { showSettings } from "@/store/slices/settingsSlice";
|
||||
|
||||
export default function CreateKnowledgeBase({
|
||||
isEdit,
|
||||
data,
|
||||
showBtn = true,
|
||||
onUpdate,
|
||||
onClose,
|
||||
}: {
|
||||
isEdit?: boolean;
|
||||
showBtn?: boolean;
|
||||
data?: Partial<KnowledgeBaseItem> | null;
|
||||
onUpdate: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [models, setModels] = useState<ModelI[]>([]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const embeddingModelOptions = models
|
||||
.filter((model) => model.type === "EMBEDDING")
|
||||
.map((model) => ({
|
||||
label: model.modelName + " (" + model.provider + ")",
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const chatModelOptions = models
|
||||
.filter((model) => model.type === "CHAT")
|
||||
.map((model) => ({
|
||||
label: model.modelName + " (" + model.provider + ")",
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const fetchModels = async () => {
|
||||
const { data } = await queryModelListUsingGet({ page: 0, size: 1000 });
|
||||
setModels(data.content || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) fetchModels();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && data) {
|
||||
setOpen(true);
|
||||
form.setFieldsValue({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
embeddingModel: data.embeddingModel,
|
||||
chatModel: data.chatModel,
|
||||
});
|
||||
}
|
||||
}, [isEdit, data, form]);
|
||||
|
||||
const handleCreateKnowledgeBase = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (isEdit && data) {
|
||||
await updateKnowledgeBaseByIdUsingPut(data.id!, values);
|
||||
message.success("知识库更新成功");
|
||||
} else {
|
||||
await createKnowledgeBaseUsingPost(values);
|
||||
message.success("知识库创建成功");
|
||||
}
|
||||
setOpen(false);
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
message.error("操作失败:", error.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpen(false);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBtn && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
创建知识库
|
||||
</Button>
|
||||
)}
|
||||
<Modal
|
||||
title={isEdit ? "编辑知识库" : "创建知识库"}
|
||||
open={open}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
maskClosable={false}
|
||||
onCancel={handleCloseModal}
|
||||
onOk={handleCreateKnowledgeBase}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="知识库名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入知识库名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入知识库名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="描述"
|
||||
name="description"
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<Input.TextArea placeholder="请输入知识库描述(可选)" rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="索引模型"
|
||||
name="embeddingModel"
|
||||
rules={[{ required: true, message: "请选择索引模型" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择索引模型"
|
||||
options={embeddingModelOptions}
|
||||
disabled={isEdit} // 编辑模式下禁用索引模型修改
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
block
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(showSettings())}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="文本理解模型"
|
||||
name="chatModel"
|
||||
rules={[{ required: true, message: "请选择文本理解模型" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择文本理解模型"
|
||||
options={chatModelOptions}
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
block
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(showSettings())}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 获取知识库列表
|
||||
export function queryKnowledgeBasesUsingPost(params: any) {
|
||||
return post("/api/knowledge-base/list", params);
|
||||
}
|
||||
|
||||
// 创建知识库
|
||||
export function createKnowledgeBaseUsingPost(data: any) {
|
||||
return post("/api/knowledge-base/create", data);
|
||||
}
|
||||
|
||||
// 获取知识库详情
|
||||
export function queryKnowledgeBaseByIdUsingGet(baseId: string) {
|
||||
return get(`/api/knowledge-base/${baseId}`);
|
||||
}
|
||||
|
||||
// 更新知识库
|
||||
export function updateKnowledgeBaseByIdUsingPut(baseId: string, data: any) {
|
||||
return put(`/api/knowledge-base/${baseId}`, data);
|
||||
}
|
||||
|
||||
// 删除知识库
|
||||
export function deleteKnowledgeBaseByIdUsingDelete(baseId: string) {
|
||||
return del(`/api/knowledge-base/${baseId}`);
|
||||
}
|
||||
|
||||
// 获取知识生成文件列表
|
||||
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data) {
|
||||
return get(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 添加文件到知识库
|
||||
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: any) {
|
||||
return post(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 删除知识生成文件
|
||||
export function deleteKnowledgeBaseFileByIdUsingDelete(baseId: string, data: any) {
|
||||
return del(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 检索知识库内容
|
||||
export function retrieveKnowledgeBaseContent(data: {
|
||||
query: string;
|
||||
topK?: number;
|
||||
threshold?: number;
|
||||
knowledgeBaseIds: string[];
|
||||
}) {
|
||||
return post("/api/knowledge-base/retrieve", data);
|
||||
}
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 获取知识库列表
|
||||
export function queryKnowledgeBasesUsingPost(params: any) {
|
||||
return post("/api/knowledge-base/list", params);
|
||||
}
|
||||
|
||||
// 创建知识库
|
||||
export function createKnowledgeBaseUsingPost(data: any) {
|
||||
return post("/api/knowledge-base/create", data);
|
||||
}
|
||||
|
||||
// 获取知识库详情
|
||||
export function queryKnowledgeBaseByIdUsingGet(baseId: string) {
|
||||
return get(`/api/knowledge-base/${baseId}`);
|
||||
}
|
||||
|
||||
// 更新知识库
|
||||
export function updateKnowledgeBaseByIdUsingPut(baseId: string, data: any) {
|
||||
return put(`/api/knowledge-base/${baseId}`, data);
|
||||
}
|
||||
|
||||
// 删除知识库
|
||||
export function deleteKnowledgeBaseByIdUsingDelete(baseId: string) {
|
||||
return del(`/api/knowledge-base/${baseId}`);
|
||||
}
|
||||
|
||||
// 获取知识生成文件列表
|
||||
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data) {
|
||||
return get(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 添加文件到知识库
|
||||
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: any) {
|
||||
return post(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 删除知识生成文件
|
||||
export function deleteKnowledgeBaseFileByIdUsingDelete(baseId: string, data: any) {
|
||||
return del(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 检索知识库内容
|
||||
export function retrieveKnowledgeBaseContent(data: {
|
||||
query: string;
|
||||
topK?: number;
|
||||
threshold?: number;
|
||||
knowledgeBaseIds: string[];
|
||||
}) {
|
||||
return post("/api/knowledge-base/retrieve", data);
|
||||
}
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
import {
|
||||
BookOpen,
|
||||
BookOpenText,
|
||||
BookType,
|
||||
ChartNoAxesColumn,
|
||||
CheckCircle,
|
||||
CircleEllipsis,
|
||||
Clock,
|
||||
Database,
|
||||
File,
|
||||
VectorSquare,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
KBFile,
|
||||
KBFileStatus,
|
||||
KBType,
|
||||
KnowledgeBaseItem,
|
||||
} from "./knowledge-base.model";
|
||||
import { formatBytes, formatDateTime, formatNumber } from "@/utils/unit";
|
||||
|
||||
export const KBFileStatusMap = {
|
||||
[KBFileStatus.PROCESSED]: {
|
||||
value: KBFileStatus.PROCESSED,
|
||||
label: "已处理",
|
||||
icon: CheckCircle,
|
||||
color: "#389e0d",
|
||||
},
|
||||
[KBFileStatus.PROCESSING]: {
|
||||
value: KBFileStatus.PROCESSING,
|
||||
label: "处理中",
|
||||
icon: Clock,
|
||||
color: "#faad14",
|
||||
},
|
||||
[KBFileStatus.PROCESS_FAILED]: {
|
||||
value: KBFileStatus.PROCESS_FAILED,
|
||||
label: "处理失败",
|
||||
icon: XCircle,
|
||||
color: "#ff4d4f",
|
||||
},
|
||||
[KBFileStatus.UNPROCESSED]: {
|
||||
value: KBFileStatus.UNPROCESSED,
|
||||
label: "未处理",
|
||||
icon: CircleEllipsis,
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
};
|
||||
|
||||
export const KBTypeMap = {
|
||||
[KBType.STRUCTURED]: {
|
||||
value: KBType.STRUCTURED,
|
||||
label: "结构化",
|
||||
icon: Database,
|
||||
iconColor: "blue",
|
||||
description: "用于处理和分析文本数据的数据集",
|
||||
},
|
||||
[KBType.UNSTRUCTURED]: {
|
||||
value: KBType.UNSTRUCTURED,
|
||||
label: "非结构化",
|
||||
icon: BookOpen,
|
||||
iconColor: "green",
|
||||
description: "适用于存储和管理各种格式的文件",
|
||||
},
|
||||
};
|
||||
|
||||
export function mapKnowledgeBase(
|
||||
kb: KnowledgeBaseItem,
|
||||
showModelFields: boolean = true
|
||||
): KnowledgeBaseItem {
|
||||
return {
|
||||
...kb,
|
||||
icon: <BookOpenText className="w-full h-full" />,
|
||||
description: kb.description,
|
||||
statistics: [
|
||||
...(showModelFields
|
||||
? [
|
||||
{
|
||||
label: "索引模型",
|
||||
key: "embeddingModel",
|
||||
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
|
||||
value:
|
||||
kb.embedding?.modelName +
|
||||
(kb.embedding?.provider
|
||||
? ` (${kb.embedding.provider})`
|
||||
: "") || "无",
|
||||
},
|
||||
{
|
||||
label: "文本理解模型",
|
||||
key: "chatModel",
|
||||
icon: <BookType className="w-4 h-4 text-blue-500" />,
|
||||
value:
|
||||
kb.chat?.modelName +
|
||||
(kb.chat?.provider ? ` (${kb.chat.provider})` : "") || "无",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "文件数",
|
||||
key: "fileCount",
|
||||
icon: <File className="w-4 h-4 text-blue-500" />,
|
||||
value: formatNumber(kb?.fileCount) || 0,
|
||||
},
|
||||
{
|
||||
label: "分块数",
|
||||
key: "chunkCount",
|
||||
icon: <ChartNoAxesColumn className="w-4 h-4 text-blue-500" />,
|
||||
value: formatNumber(kb?.chunkCount) || 0,
|
||||
},
|
||||
],
|
||||
updatedAt: formatDateTime(kb.updatedAt),
|
||||
createdAt: formatDateTime(kb.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFileData(file: Partial<KBFile>): KBFile {
|
||||
return {
|
||||
...file,
|
||||
name: file.fileName,
|
||||
createdAt: formatDateTime(file.createdAt),
|
||||
updatedAt: formatDateTime(file.updatedAt),
|
||||
status: KBFileStatusMap[file.status] || {
|
||||
value: file.status,
|
||||
label: "未知状态",
|
||||
icon: CircleEllipsis,
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const DatasetFileCols = [
|
||||
{
|
||||
title: "所属数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
ellipsis: true,
|
||||
render: formatBytes,
|
||||
},
|
||||
];
|
||||
import {
|
||||
BookOpen,
|
||||
BookOpenText,
|
||||
BookType,
|
||||
ChartNoAxesColumn,
|
||||
CheckCircle,
|
||||
CircleEllipsis,
|
||||
Clock,
|
||||
Database,
|
||||
File,
|
||||
VectorSquare,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
KBFile,
|
||||
KBFileStatus,
|
||||
KBType,
|
||||
KnowledgeBaseItem,
|
||||
} from "./knowledge-base.model";
|
||||
import { formatBytes, formatDateTime, formatNumber } from "@/utils/unit";
|
||||
|
||||
export const KBFileStatusMap = {
|
||||
[KBFileStatus.PROCESSED]: {
|
||||
value: KBFileStatus.PROCESSED,
|
||||
label: "已处理",
|
||||
icon: CheckCircle,
|
||||
color: "#389e0d",
|
||||
},
|
||||
[KBFileStatus.PROCESSING]: {
|
||||
value: KBFileStatus.PROCESSING,
|
||||
label: "处理中",
|
||||
icon: Clock,
|
||||
color: "#faad14",
|
||||
},
|
||||
[KBFileStatus.PROCESS_FAILED]: {
|
||||
value: KBFileStatus.PROCESS_FAILED,
|
||||
label: "处理失败",
|
||||
icon: XCircle,
|
||||
color: "#ff4d4f",
|
||||
},
|
||||
[KBFileStatus.UNPROCESSED]: {
|
||||
value: KBFileStatus.UNPROCESSED,
|
||||
label: "未处理",
|
||||
icon: CircleEllipsis,
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
};
|
||||
|
||||
export const KBTypeMap = {
|
||||
[KBType.STRUCTURED]: {
|
||||
value: KBType.STRUCTURED,
|
||||
label: "结构化",
|
||||
icon: Database,
|
||||
iconColor: "blue",
|
||||
description: "用于处理和分析文本数据的数据集",
|
||||
},
|
||||
[KBType.UNSTRUCTURED]: {
|
||||
value: KBType.UNSTRUCTURED,
|
||||
label: "非结构化",
|
||||
icon: BookOpen,
|
||||
iconColor: "green",
|
||||
description: "适用于存储和管理各种格式的文件",
|
||||
},
|
||||
};
|
||||
|
||||
export function mapKnowledgeBase(
|
||||
kb: KnowledgeBaseItem,
|
||||
showModelFields: boolean = true
|
||||
): KnowledgeBaseItem {
|
||||
return {
|
||||
...kb,
|
||||
icon: <BookOpenText className="w-full h-full" />,
|
||||
description: kb.description,
|
||||
statistics: [
|
||||
...(showModelFields
|
||||
? [
|
||||
{
|
||||
label: "索引模型",
|
||||
key: "embeddingModel",
|
||||
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
|
||||
value:
|
||||
kb.embedding?.modelName +
|
||||
(kb.embedding?.provider
|
||||
? ` (${kb.embedding.provider})`
|
||||
: "") || "无",
|
||||
},
|
||||
{
|
||||
label: "文本理解模型",
|
||||
key: "chatModel",
|
||||
icon: <BookType className="w-4 h-4 text-blue-500" />,
|
||||
value:
|
||||
kb.chat?.modelName +
|
||||
(kb.chat?.provider ? ` (${kb.chat.provider})` : "") || "无",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "文件数",
|
||||
key: "fileCount",
|
||||
icon: <File className="w-4 h-4 text-blue-500" />,
|
||||
value: formatNumber(kb?.fileCount) || 0,
|
||||
},
|
||||
{
|
||||
label: "分块数",
|
||||
key: "chunkCount",
|
||||
icon: <ChartNoAxesColumn className="w-4 h-4 text-blue-500" />,
|
||||
value: formatNumber(kb?.chunkCount) || 0,
|
||||
},
|
||||
],
|
||||
updatedAt: formatDateTime(kb.updatedAt),
|
||||
createdAt: formatDateTime(kb.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFileData(file: Partial<KBFile>): KBFile {
|
||||
return {
|
||||
...file,
|
||||
name: file.fileName,
|
||||
createdAt: formatDateTime(file.createdAt),
|
||||
updatedAt: formatDateTime(file.updatedAt),
|
||||
status: KBFileStatusMap[file.status] || {
|
||||
value: file.status,
|
||||
label: "未知状态",
|
||||
icon: CircleEllipsis,
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const DatasetFileCols = [
|
||||
{
|
||||
title: "所属数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
ellipsis: true,
|
||||
render: formatBytes,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
export enum KBFileStatus {
|
||||
UNPROCESSED = "UNPROCESSED",
|
||||
PROCESSING = "PROCESSING",
|
||||
PROCESSED = "PROCESSED",
|
||||
PROCESS_FAILED = "PROCESS_FAILED",
|
||||
}
|
||||
|
||||
export enum KBType {
|
||||
UNSTRUCTURED = "unstructured",
|
||||
STRUCTURED = "structured",
|
||||
}
|
||||
|
||||
export interface KnowledgeBaseItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: KBType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
embeddingModel: string;
|
||||
chatModel: string;
|
||||
fileCount: number;
|
||||
chunkCount: number;
|
||||
embedding: never;
|
||||
chat: never;
|
||||
}
|
||||
|
||||
export interface KBFile {
|
||||
id: string;
|
||||
fileName: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: KBFileStatus;
|
||||
chunkCount: number;
|
||||
metadata: Record<string, any>;
|
||||
knowledgeBaseId: string;
|
||||
fileId: string;
|
||||
updatedBy: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
interface Chunk {
|
||||
id: number;
|
||||
content: string;
|
||||
position: number;
|
||||
tokens: number;
|
||||
embedding?: number[];
|
||||
similarity?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
vectorId?: string;
|
||||
sliceOperator?: string;
|
||||
parentChunkId?: number;
|
||||
metadata?: {
|
||||
source: string;
|
||||
page?: number;
|
||||
section?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface VectorizationRecord {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
operation: "create" | "update" | "delete" | "reprocess";
|
||||
fileId: number;
|
||||
fileName: string;
|
||||
chunksProcessed: number;
|
||||
vectorsGenerated: number;
|
||||
status: "success" | "failed" | "partial";
|
||||
duration: string;
|
||||
config: {
|
||||
embeddingModel: string;
|
||||
chunkSize: number;
|
||||
sliceMethod: string;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
export enum KBFileStatus {
|
||||
UNPROCESSED = "UNPROCESSED",
|
||||
PROCESSING = "PROCESSING",
|
||||
PROCESSED = "PROCESSED",
|
||||
PROCESS_FAILED = "PROCESS_FAILED",
|
||||
}
|
||||
|
||||
export enum KBType {
|
||||
UNSTRUCTURED = "unstructured",
|
||||
STRUCTURED = "structured",
|
||||
}
|
||||
|
||||
export interface KnowledgeBaseItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: KBType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
embeddingModel: string;
|
||||
chatModel: string;
|
||||
fileCount: number;
|
||||
chunkCount: number;
|
||||
embedding: never;
|
||||
chat: never;
|
||||
}
|
||||
|
||||
export interface KBFile {
|
||||
id: string;
|
||||
fileName: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: KBFileStatus;
|
||||
chunkCount: number;
|
||||
metadata: Record<string, any>;
|
||||
knowledgeBaseId: string;
|
||||
fileId: string;
|
||||
updatedBy: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
interface Chunk {
|
||||
id: number;
|
||||
content: string;
|
||||
position: number;
|
||||
tokens: number;
|
||||
embedding?: number[];
|
||||
similarity?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
vectorId?: string;
|
||||
sliceOperator?: string;
|
||||
parentChunkId?: number;
|
||||
metadata?: {
|
||||
source: string;
|
||||
page?: number;
|
||||
section?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface VectorizationRecord {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
operation: "create" | "update" | "delete" | "reprocess";
|
||||
fileId: number;
|
||||
fileName: string;
|
||||
chunksProcessed: number;
|
||||
vectorsGenerated: number;
|
||||
status: "success" | "failed" | "partial";
|
||||
duration: string;
|
||||
config: {
|
||||
embeddingModel: string;
|
||||
chunkSize: number;
|
||||
sliceMethod: string;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user