You've already forked DataMate
knowledge base pages (#43)
* feat: Update site name to DataMate and refine text for AI data processing * feat: Refactor settings page and implement model access functionality - Created a new ModelAccess component for managing model configurations. - Removed the old Settings component and replaced it with a new SettingsPage component that integrates ModelAccess, SystemConfig, and WebhookConfig. - Added SystemConfig component for managing system settings. - Implemented WebhookConfig component for managing webhook configurations. - Updated API functions for model management in settings.apis.ts. - Adjusted routing to point to the new SettingsPage component. * feat: Implement Data Collection Page with Task Management and Execution Log - Created DataCollectionPage component to manage data collection tasks. - Added TaskManagement and ExecutionLog components for task handling and logging. - Integrated task operations including start, stop, edit, and delete functionalities. - Implemented filtering and searching capabilities in task management. - Introduced SimpleCronScheduler for scheduling tasks with cron expressions. - Updated CreateTask component to utilize new scheduling and template features. - Enhanced BasicInformation component to conditionally render fields based on visibility settings. - Refactored ImportConfiguration component to remove NAS import section. * feat: Update task creation API endpoint and enhance task creation form with new fields and validation * Refactor file upload and operator management components - Removed unnecessary console logs from file download and export functions. - Added size property to TaskItem interface for better task management. - Simplified TaskUpload component by utilizing useFileSliceUpload hook for file upload logic. - Enhanced OperatorPluginCreate component to handle file uploads and parsing more efficiently. - Updated ConfigureStep component to use Ant Design Form for better data handling and validation. - Improved PreviewStep component to navigate back to the operator market. - Added support for additional file types in UploadStep component. - Implemented delete operator functionality in OperatorMarketPage with confirmation prompts. - Cleaned up unused API functions in operator.api.ts to streamline the codebase. - Fixed number formatting utility to handle zero values correctly. * Refactor Knowledge Generation to Knowledge Base - Created new API service for Knowledge Base operations including querying, creating, updating, and deleting knowledge bases and files. - Added constants for Knowledge Base status and type mappings. - Defined models for Knowledge Base and related files. - Removed obsolete Knowledge Base creation and home components, replacing them with new implementations under the Knowledge Base structure. - Updated routing to reflect the new Knowledge Base paths. - Adjusted menu items to align with the new Knowledge Base terminology. - Modified ModelAccess interface to include modelName and type properties. * feat: Implement Knowledge Base Page with CRUD operations and data management - Added KnowledgeBasePage component for displaying and managing knowledge bases. - Integrated search and filter functionalities with SearchControls component. - Implemented CreateKnowledgeBase component for creating and editing knowledge bases. - Enhanced AddDataDialog for file uploads and dataset selections. - Introduced TableTransfer component for managing data transfers between tables. - Updated API functions for knowledge base operations, including file management. - Refactored knowledge base model to include file status and metadata. - Adjusted routing to point to the new KnowledgeBasePage.
This commit is contained in:
@@ -39,7 +39,7 @@ interface DetailHeaderProps<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DetailHeader<T>({
|
function DetailHeader<T>({
|
||||||
data,
|
data = {} as T,
|
||||||
statistics,
|
statistics,
|
||||||
operations,
|
operations,
|
||||||
tagConfig,
|
tagConfig,
|
||||||
@@ -59,7 +59,7 @@ function DetailHeader<T>({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h1 className="text-lg font-bold text-gray-900">{data.name}</h1>
|
<h1 className="text-lg font-bold text-gray-900">{data?.name}</h1>
|
||||||
{data?.status && (
|
{data?.status && (
|
||||||
<Tag color={data.status?.color}>
|
<Tag color={data.status?.color}>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
@@ -86,7 +86,7 @@ function DetailHeader<T>({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="text-gray-700 mb-4">{data.description}</p>
|
<p className="text-gray-700 mb-4">{data?.description}</p>
|
||||||
<div className="flex items-center gap-6 text-sm">
|
<div className="flex items-center gap-6 text-sm">
|
||||||
{statistics.map((stat) => (
|
{statistics.map((stat) => (
|
||||||
<div key={stat.key} className="flex items-center gap-1">
|
<div key={stat.key} className="flex items-center gap-1">
|
||||||
@@ -112,13 +112,10 @@ function DetailHeader<T>({
|
|||||||
<Tooltip key={op.key} title={op.label}>
|
<Tooltip key={op.key} title={op.label}>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key={op.key}
|
key={op.key}
|
||||||
title={op.confirm.title}
|
{...op.confirm}
|
||||||
description={op.confirm.description}
|
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
op?.onClick();
|
op?.confirm?.onConfirm?.();
|
||||||
}}
|
}}
|
||||||
okText={op.confirm.okText || "确定"}
|
|
||||||
cancelText={op.confirm.cancelText || "取消"}
|
|
||||||
okType={op.danger ? "danger" : "primary"}
|
okType={op.danger ? "danger" : "primary"}
|
||||||
overlayStyle={{ zIndex: 9999 }}
|
overlayStyle={{ zIndex: 9999 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function useFetchData<T>(
|
|||||||
fetchFunc: (params?: any) => Promise<any>,
|
fetchFunc: (params?: any) => Promise<any>,
|
||||||
mapDataFunc: (data: Partial<T>) => T = (data) => data as T,
|
mapDataFunc: (data: Partial<T>) => T = (data) => data as T,
|
||||||
pollingInterval: number = 30000, // 默认30秒轮询一次
|
pollingInterval: number = 30000, // 默认30秒轮询一次
|
||||||
autoRefresh: boolean = true,
|
autoRefresh: boolean = false, // 是否自动开始轮询,默认 false
|
||||||
additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数
|
additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数
|
||||||
pageOffset: number = 1
|
pageOffset: number = 1
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -120,10 +120,11 @@ const MockAPI = {
|
|||||||
queryKnowledgeBaseByIdUsingGet: "/knowledge-base/:baseId", // 根据ID获取知识库详情
|
queryKnowledgeBaseByIdUsingGet: "/knowledge-base/:baseId", // 根据ID获取知识库详情
|
||||||
updateKnowledgeBaseByIdUsingPut: "/knowledge-base/:baseId", // 更新知识库
|
updateKnowledgeBaseByIdUsingPut: "/knowledge-base/:baseId", // 更新知识库
|
||||||
deleteKnowledgeBaseByIdUsingDelete: "/knowledge-base/:baseId", // 删除知识库
|
deleteKnowledgeBaseByIdUsingDelete: "/knowledge-base/:baseId", // 删除知识库
|
||||||
queryKnowledgeGenerationTasksUsingPost: "/knowledge-base/tasks", // 获取知识生成任务列表
|
addKnowledgeBaseFilesUsingPost: "/knowledge-base/:baseId/files", // 添加文件到知识库
|
||||||
addKnowledgeGenerationFilesUsingPost: "/knowledge-base/:baseId/files", // 添加文件到知识库
|
queryKnowledgeBaseFilesGet: "/knowledge-base/:baseId/files", // 根据ID获取知识生成文件列表
|
||||||
queryKnowledgeGenerationFilesByIdUsingGet: "/knowledge-base/:baseId/files/:fileId", // 根据ID获取知识生成文件详情
|
queryKnowledgeBaseFilesByIdUsingGet:
|
||||||
deleteKnowledgeGenerationTaskByIdUsingDelete: "/knowledge-base/:baseId/files", // 删除知识生成文件
|
"/knowledge-base/:baseId/files/:fileId", // 根据ID获取知识生成文件详情
|
||||||
|
deleteKnowledgeBaseTaskByIdUsingDelete: "/knowledge-base/:baseId/files/:id", // 删除知识生成文件
|
||||||
|
|
||||||
// 算子市场
|
// 算子市场
|
||||||
queryOperatorsUsingPost: "/operators/list", // 获取算子列表
|
queryOperatorsUsingPost: "/operators/list", // 获取算子列表
|
||||||
|
|||||||
@@ -161,8 +161,6 @@ module.exports = function (router) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (type) {
|
if (type) {
|
||||||
console.log("filter type:", type);
|
|
||||||
|
|
||||||
filteredDatasets = filteredDatasets.filter(
|
filteredDatasets = filteredDatasets.filter(
|
||||||
(dataset) => dataset.datasetType === type
|
(dataset) => dataset.datasetType === type
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,29 @@ function KnowledgeBaseItem() {
|
|||||||
|
|
||||||
const knowledgeBaseList = new Array(50).fill(null).map(KnowledgeBaseItem);
|
const knowledgeBaseList = new Array(50).fill(null).map(KnowledgeBaseItem);
|
||||||
|
|
||||||
|
function fileItem() {
|
||||||
|
return {
|
||||||
|
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
|
||||||
|
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
||||||
|
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
||||||
|
createdBy: Mock.Random.cname(),
|
||||||
|
updatedBy: Mock.Random.cname(),
|
||||||
|
knowledgeBaseId: Mock.Random.pick(knowledgeBaseList).id,
|
||||||
|
fileId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
|
||||||
|
fileName: Mock.Random.ctitle(5, 15),
|
||||||
|
chunkCount: Mock.Random.integer(1, 100),
|
||||||
|
metadata: {},
|
||||||
|
status: Mock.Random.pick([
|
||||||
|
"UNPROCESSED",
|
||||||
|
"PROCESSING",
|
||||||
|
"PROCESSED",
|
||||||
|
"PROCESS_FAILED",
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileList = new Array(20).fill(null).map(fileItem);
|
||||||
|
|
||||||
module.exports = function (router) {
|
module.exports = function (router) {
|
||||||
// 获取知识库列表
|
// 获取知识库列表
|
||||||
router.post(API.queryKnowledgeBasesUsingPost, (req, res) => {
|
router.post(API.queryKnowledgeBasesUsingPost, (req, res) => {
|
||||||
@@ -56,15 +79,16 @@ module.exports = function (router) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 获取知识库详情
|
// 获取知识库详情
|
||||||
router.get(
|
router.get(API.queryKnowledgeBaseByIdUsingGet, (req, res) => {
|
||||||
new RegExp(API.queryKnowledgeBaseByIdUsingGet.replace(":baseId", "(\\w+)")),
|
const id = req.params.baseId;
|
||||||
(req, res) => {
|
const item =
|
||||||
const id = req.params.baseId;
|
knowledgeBaseList.find((kb) => kb.id === id) || KnowledgeBaseItem();
|
||||||
const item =
|
res.send({
|
||||||
knowledgeBaseList.find((kb) => kb.id === id) || KnowledgeBaseItem();
|
code: "0",
|
||||||
res.send(item);
|
msg: "Success",
|
||||||
}
|
data: item,
|
||||||
);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 更新知识库
|
// 更新知识库
|
||||||
router.put(API.updateKnowledgeBaseByIdUsingPut, (req, res) => {
|
router.put(API.updateKnowledgeBaseByIdUsingPut, (req, res) => {
|
||||||
@@ -90,72 +114,63 @@ module.exports = function (router) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取知识生成任务列表
|
// 添加文件到知识库
|
||||||
router.post(API.queryKnowledgeGenerationTasksUsingPost, (req, res) => {
|
router.post(API.addKnowledgeBaseFilesUsingPost, (req, res) => {
|
||||||
const tasks = Mock.mock({
|
const file = Mock.mock({
|
||||||
"data|10": [
|
id: "@guid",
|
||||||
{
|
name: "@ctitle(5,15)",
|
||||||
id: "@guid",
|
size: "@integer(1000,1000000)",
|
||||||
name: "@ctitle(5,15)",
|
status: "uploaded",
|
||||||
status: '@pick(["pending","running","success","failed"])',
|
createdAt: "@datetime",
|
||||||
createdAt: "@datetime",
|
|
||||||
updatedAt: "@datetime",
|
|
||||||
progress: "@integer(0,100)",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 10,
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
});
|
||||||
res.send(tasks);
|
res.status(201).send(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加文件到知识库
|
|
||||||
router.post(
|
|
||||||
new RegExp(
|
|
||||||
API.addKnowledgeGenerationFilesUsingPost.replace(":baseId", "(\\w+)")
|
|
||||||
),
|
|
||||||
(req, res) => {
|
|
||||||
const file = Mock.mock({
|
|
||||||
id: "@guid",
|
|
||||||
name: "@ctitle(5,15)",
|
|
||||||
size: "@integer(1000,1000000)",
|
|
||||||
status: "uploaded",
|
|
||||||
createdAt: "@datetime",
|
|
||||||
});
|
|
||||||
res.status(201).send(file);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取知识生成文件详情
|
// 获取知识生成文件详情
|
||||||
router.get(
|
router.get(API.queryKnowledgeBaseFilesGet, (req, res) => {
|
||||||
new RegExp(
|
const { keyword, page, size } = req.query;
|
||||||
API.queryKnowledgeGenerationFilesByIdUsingGet
|
let filteredList = fileList;
|
||||||
.replace(":baseId", "(\\w+)")
|
if (keyword) {
|
||||||
.replace(":fileId", "(\\w+)")
|
filteredList = fileList.filter((file) => file.fileName.includes(keyword));
|
||||||
),
|
|
||||||
(req, res) => {
|
|
||||||
const file = Mock.mock({
|
|
||||||
id: req.params.fileId,
|
|
||||||
name: "@ctitle(5,15)",
|
|
||||||
size: "@integer(1000,1000000)",
|
|
||||||
status: "uploaded",
|
|
||||||
createdAt: "@datetime",
|
|
||||||
});
|
|
||||||
res.send(file);
|
|
||||||
}
|
}
|
||||||
);
|
const start = page * size;
|
||||||
|
const end = start + size;
|
||||||
|
const totalElements = filteredList.length;
|
||||||
|
const paginatedList = filteredList.slice(start, end);
|
||||||
|
res.send({
|
||||||
|
code: "0",
|
||||||
|
msg: "Success",
|
||||||
|
data: {
|
||||||
|
totalElements,
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
content: paginatedList,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get(API.queryKnowledgeBaseFilesByIdUsingGet, (req, res) => {
|
||||||
|
const { baseId, fileId } = req.params;
|
||||||
|
const item =
|
||||||
|
fileList.find(
|
||||||
|
(file) => file.knowledgeBaseId === baseId && file.id === fileId
|
||||||
|
) || fileItem();
|
||||||
|
res.send({
|
||||||
|
code: "0",
|
||||||
|
msg: "Success",
|
||||||
|
data: item,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 删除知识生成文件
|
// 删除知识生成文件
|
||||||
router.delete(
|
router.delete(API.deleteKnowledgeBaseTaskByIdUsingDelete, (req, res) => {
|
||||||
new RegExp(
|
const { id } = req.params;
|
||||||
API.deleteKnowledgeGenerationTaskByIdUsingDelete.replace(
|
const idx = fileList.findIndex((file) => file.id === id);
|
||||||
":baseId",
|
if (idx >= 0) {
|
||||||
"(\\w+)"
|
fileList.splice(idx, 1);
|
||||||
)
|
res.status(200).send({ success: true });
|
||||||
),
|
return;
|
||||||
(req, res) => {
|
|
||||||
res.send({ success: true });
|
|
||||||
}
|
}
|
||||||
);
|
res.status(404).send({ message: "Not found" });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ export function mapDataset(dataset: AnyObject): Dataset {
|
|||||||
datasetTypeMap[dataset?.datasetType] || {};
|
datasetTypeMap[dataset?.datasetType] || {};
|
||||||
return {
|
return {
|
||||||
...dataset,
|
...dataset,
|
||||||
|
key: dataset.id,
|
||||||
type: datasetTypeMap[dataset.datasetType]?.label || "未知",
|
type: datasetTypeMap[dataset.datasetType]?.label || "未知",
|
||||||
size: formatBytes(dataset.totalSize || 0),
|
size: formatBytes(dataset.totalSize || 0),
|
||||||
createdAt: formatDateTime(dataset.createdAt) || "--",
|
createdAt: formatDateTime(dataset.createdAt) || "--",
|
||||||
|
|||||||
@@ -1,628 +1,236 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Table, Badge, Button, Breadcrumb, Tooltip, App } from "antd";
|
||||||
import {
|
import {
|
||||||
Plus,
|
DeleteOutlined,
|
||||||
Edit,
|
EditOutlined,
|
||||||
File,
|
ReloadOutlined,
|
||||||
Trash2,
|
} from "@ant-design/icons";
|
||||||
Save,
|
import { useNavigate, useParams } from "react-router";
|
||||||
Layers,
|
|
||||||
RefreshCw,
|
|
||||||
BookOpen,
|
|
||||||
Database,
|
|
||||||
MoreHorizontal,
|
|
||||||
Upload,
|
|
||||||
Zap,
|
|
||||||
StarOff,
|
|
||||||
CheckCircle,
|
|
||||||
VectorSquareIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Progress,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
message,
|
|
||||||
Card,
|
|
||||||
Breadcrumb,
|
|
||||||
Checkbox,
|
|
||||||
Dropdown,
|
|
||||||
} from "antd";
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import DetailHeader from "@/components/DetailHeader";
|
import DetailHeader from "@/components/DetailHeader";
|
||||||
import { SearchControls } from "@/components/SearchControls";
|
import { SearchControls } from "@/components/SearchControls";
|
||||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
|
||||||
|
import { mapFileData, mapKnowledgeBase } from "../knowledge-base.const";
|
||||||
|
import {
|
||||||
|
deleteKnowledgeBaseByIdUsingDelete,
|
||||||
|
deleteKnowledgeBaseFileByIdUsingDelete,
|
||||||
|
queryKnowledgeBaseByIdUsingGet,
|
||||||
|
queryKnowledgeBaseFilesUsingGet,
|
||||||
|
} from "../knowledge-base.api";
|
||||||
|
import useFetchData from "@/hooks/useFetchData";
|
||||||
|
import AddDataDialog from "../components/AddDataDialog";
|
||||||
|
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
|
||||||
|
|
||||||
const KnowledgeBaseDetailPage: React.FC = () => {
|
const KnowledgeBaseDetailPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem>(null);
|
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem>(null);
|
||||||
const [files, setFiles] = useState([]);
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
|
|
||||||
|
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,
|
||||||
|
} = useFetchData<KBFile>(
|
||||||
|
(params) => queryKnowledgeBaseFilesUsingGet(knowledgeBase?.id, params),
|
||||||
|
mapFileData
|
||||||
|
);
|
||||||
|
|
||||||
// File table logic
|
// File table logic
|
||||||
const handleDeleteFile = (file: KBFile) => {};
|
const handleDeleteFile = async (file: KBFile) => {
|
||||||
|
try {
|
||||||
|
await deleteKnowledgeBaseFileByIdUsingDelete(knowledgeBase.id, file.id);
|
||||||
const handleDeleteKB = (kb: KnowledgeBase) => {};
|
message.success("文件已删除");
|
||||||
|
fetchFiles();
|
||||||
// 状态 Badge 映射
|
} catch (error) {
|
||||||
function getStatusBadgeVariant(status: string) {
|
message.error("文件删除失败");
|
||||||
switch (status) {
|
|
||||||
case "completed":
|
|
||||||
case "ready":
|
|
||||||
return "success";
|
|
||||||
case "processing":
|
|
||||||
case "vectorizing":
|
|
||||||
return "processing";
|
|
||||||
case "importing":
|
|
||||||
return "warning";
|
|
||||||
case "error":
|
|
||||||
return "error";
|
|
||||||
default:
|
|
||||||
return "default";
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
function getStatusLabel(status: string) {
|
|
||||||
switch (status) {
|
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||||
case "completed":
|
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||||
case "ready":
|
message.success("知识库已删除");
|
||||||
return "已完成";
|
navigate("/data/knowledge-base");
|
||||||
case "processing":
|
};
|
||||||
return "处理中";
|
|
||||||
case "vectorizing":
|
const handleRefreshPage = () => {
|
||||||
return "向量化中";
|
fetchKnowledgeBaseDetails(knowledgeBase.id);
|
||||||
case "importing":
|
fetchFiles();
|
||||||
return "导入中";
|
setShowEdit(false);
|
||||||
case "error":
|
};
|
||||||
return "错误";
|
|
||||||
case "disabled":
|
const operations = [
|
||||||
return "已禁用";
|
{
|
||||||
default:
|
key: "edit",
|
||||||
return "未知";
|
label: "编辑知识库",
|
||||||
}
|
icon: <EditOutlined className="w-4 h-4" />,
|
||||||
}
|
onClick: () => {
|
||||||
function getStatusIcon(status: string) {
|
setShowEdit(true);
|
||||||
switch (status) {
|
},
|
||||||
case "completed":
|
},
|
||||||
case "ready":
|
{
|
||||||
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
key: "refresh",
|
||||||
case "processing":
|
label: "刷新知识库",
|
||||||
case "vectorizing":
|
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||||
return <RefreshCw className="w-4 h-4 text-blue-500 animate-spin" />;
|
onClick: () => {
|
||||||
case "importing":
|
handleRefreshPage();
|
||||||
return <Upload className="w-4 h-4 text-orange-500" />;
|
},
|
||||||
case "error":
|
},
|
||||||
return <Trash2 className="w-4 h-4 text-red-500" />;
|
{
|
||||||
default:
|
key: "delete",
|
||||||
return <File className="w-4 h-4 text-gray-400" />;
|
label: "删除知识库",
|
||||||
}
|
danger: true,
|
||||||
}
|
confirm: {
|
||||||
|
title: "确认删除该知识库吗?",
|
||||||
|
description: "删除后将无法恢复,请谨慎操作。",
|
||||||
|
cancelText: "取消",
|
||||||
|
okText: "删除",
|
||||||
|
okType: "danger",
|
||||||
|
onConfirm: () => 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 = [
|
const fileColumns = [
|
||||||
{
|
{
|
||||||
title: "文件名",
|
title: "文件名",
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
key: "name",
|
key: "name",
|
||||||
filterDropdown: ({
|
width: 200,
|
||||||
setSelectedKeys,
|
ellipsis: true,
|
||||||
selectedKeys,
|
fixed: "left" as const,
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
}: any) => (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Input
|
|
||||||
placeholder="搜索文件名"
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={confirm}
|
|
||||||
style={{ width: 188, marginBottom: 8, display: "block" }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={confirm}
|
|
||||||
size="small"
|
|
||||||
style={{ width: 90, marginRight: 8 }}
|
|
||||||
>
|
|
||||||
搜索
|
|
||||||
</Button>
|
|
||||||
<Button onClick={clearFilters} size="small" style={{ width: 90 }}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
onFilter: (value: string, record: KBFile) =>
|
|
||||||
record.name.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
render: (text: string, file: KBFile) => (
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
onClick={() =>
|
|
||||||
navigate("/data/knowledge-base/file-detail/" + file.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{file.name}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "类型",
|
title: "状态",
|
||||||
dataIndex: "type",
|
dataIndex: "status",
|
||||||
key: "type",
|
|
||||||
filters: allFileTypes.map((type) => ({
|
|
||||||
text: type,
|
|
||||||
value: type,
|
|
||||||
})),
|
|
||||||
onFilter: (value: string, record: KBFile) => record.type === value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "大小",
|
|
||||||
dataIndex: "size",
|
|
||||||
key: "size",
|
|
||||||
sorter: (a: KBFile, b: KBFile) => parseFloat(a.size) - parseFloat(b.size),
|
|
||||||
sortOrder: fileSortOrder,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "向量化状态",
|
|
||||||
dataIndex: "vectorizationStatus",
|
|
||||||
key: "vectorizationStatus",
|
key: "vectorizationStatus",
|
||||||
filters: allVectorizationStatuses
|
width: 120,
|
||||||
.filter((opt) => opt.value !== null)
|
render: (status: any) => (
|
||||||
.map((opt) => ({
|
<Badge color={status?.color} text={status?.label} />
|
||||||
text: opt.label,
|
|
||||||
value: opt.value,
|
|
||||||
})),
|
|
||||||
onFilter: (value: string, record: KBFile) =>
|
|
||||||
record.vectorizationStatus === value,
|
|
||||||
render: (_: any, file: KBFile) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
status={getStatusBadgeVariant(
|
|
||||||
file.vectorizationStatus || "pending"
|
|
||||||
)}
|
|
||||||
text={getStatusLabel(file.vectorizationStatus || "pending")}
|
|
||||||
/>
|
|
||||||
{file.vectorizationStatus === "processing" && (
|
|
||||||
<div className="w-16">
|
|
||||||
<Progress percent={file.progress} size="small" showInfo={false} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "来源",
|
|
||||||
dataIndex: "source",
|
|
||||||
key: "source",
|
|
||||||
render: (_: any, file: KBFile) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
status={file.source === "upload" ? "processing" : "default"}
|
|
||||||
text={file.source === "upload" ? "上传" : "数据集"}
|
|
||||||
/>
|
|
||||||
{file.datasetId && (
|
|
||||||
<span className="text-xs text-gray-500">({file.datasetId})</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "分块数",
|
title: "分块数",
|
||||||
dataIndex: "chunkCount",
|
dataIndex: "chunkCount",
|
||||||
key: "chunkCount",
|
key: "chunkCount",
|
||||||
render: (chunkCount: number) => (
|
width: 100,
|
||||||
<span className="font-medium text-gray-900">{chunkCount}</span>
|
ellipsis: true,
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "上传时间",
|
title: "创建时间",
|
||||||
dataIndex: "uploadedAt",
|
dataIndex: "createdAt",
|
||||||
key: "uploadedAt",
|
key: "createdAt",
|
||||||
|
ellipsis: true,
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "更新时间",
|
||||||
|
dataIndex: "updatedAt",
|
||||||
|
key: "updatedAt",
|
||||||
|
ellipsis: true,
|
||||||
|
width: 180,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
align: "right" as const,
|
align: "right" as const,
|
||||||
|
width: 100,
|
||||||
render: (_: any, file: KBFile) => (
|
render: (_: any, file: KBFile) => (
|
||||||
<Dropdown
|
<div>
|
||||||
menu={{
|
{fileOps.map((op) => (
|
||||||
items: [
|
<Tooltip key={op.key} title={op.label}>
|
||||||
{
|
<Button
|
||||||
label: "重试",
|
type="text"
|
||||||
key: "retry",
|
icon={op.icon}
|
||||||
onClick: () => handleStartVectorization(file.id),
|
danger={op?.danger}
|
||||||
},
|
onClick={() => op.onClick(file)}
|
||||||
{
|
/>
|
||||||
label: "删除",
|
</Tooltip>
|
||||||
key: "delete",
|
))}
|
||||||
onClick: () => handleDeleteFile(file),
|
</div>
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MoreHorizontal />
|
|
||||||
</Dropdown>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="h-full flex flex-col">
|
||||||
{/* Breadcrumb */}
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb.Item>
|
||||||
<a onClick={() => navigate("/data/knowledge-base")}>知识库</a>
|
<a onClick={() => navigate("/data/knowledge-base")}>知识库</a>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>{knowledgeBase.name}</Breadcrumb.Item>
|
<Breadcrumb.Item>{knowledgeBase?.name}</Breadcrumb.Item>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<DetailHeader
|
||||||
{/* Knowledge Base Header */}
|
data={knowledgeBase}
|
||||||
<DetailHeader
|
statistics={knowledgeBase?.statistics || []}
|
||||||
data={{
|
operations={operations}
|
||||||
icon:
|
/>
|
||||||
knowledgeBase.type === "structured" ? (
|
<CreateKnowledgeBase
|
||||||
<Database className="w-8 h-8" />
|
showBtn={false}
|
||||||
) : (
|
isEdit={showEdit}
|
||||||
<BookOpen className="w-8 h-8" />
|
data={knowledgeBase}
|
||||||
),
|
onUpdate={handleRefreshPage}
|
||||||
status: {
|
onClose={() => setShowEdit(false)}
|
||||||
label: getStatusLabel(knowledgeBase.status),
|
/>
|
||||||
icon: getStatusIcon(knowledgeBase.status),
|
<div className="flex-1 border-card p-6 mt-4">
|
||||||
color: getStatusBadgeVariant(knowledgeBase.status),
|
<div className="flex items-center justify-between mb-4 gap-3">
|
||||||
},
|
<div className="flex-1">
|
||||||
name: knowledgeBase.name,
|
<SearchControls
|
||||||
description: knowledgeBase.description,
|
searchTerm={searchParams.keyword}
|
||||||
createdAt: knowledgeBase.createdAt,
|
onSearchChange={(keyword) =>
|
||||||
lastUpdated: knowledgeBase.lastUpdated,
|
setSearchParams({ ...searchParams, keyword })
|
||||||
}}
|
}
|
||||||
statistics={[
|
searchPlaceholder="搜索文件名..."
|
||||||
{
|
filters={[]}
|
||||||
icon: <File className="w-4 h-4 text-gray-400" />,
|
onFiltersChange={handleFiltersChange}
|
||||||
label: "文件",
|
onClearFilters={() =>
|
||||||
value: knowledgeBase.fileCount,
|
setSearchParams({ ...searchParams, filter: {} })
|
||||||
},
|
}
|
||||||
{
|
showViewToggle={false}
|
||||||
icon: <Layers className="w-4 h-4 text-gray-400" />,
|
showReload={false}
|
||||||
label: "分块",
|
/>
|
||||||
value: knowledgeBase.chunkCount?.toLocaleString?.() ?? 0,
|
</div>
|
||||||
},
|
<AddDataDialog knowledgeBase={knowledgeBase} />
|
||||||
{
|
</div>
|
||||||
icon: <StarOff className="w-4 h-4 text-gray-400" />,
|
|
||||||
label: "向量",
|
<Table
|
||||||
value: knowledgeBase.vectorCount?.toLocaleString?.() ?? 0,
|
loading={loading}
|
||||||
},
|
columns={fileColumns}
|
||||||
{
|
dataSource={files}
|
||||||
icon: <Database className="w-4 h-4 text-gray-400" />,
|
rowKey="id"
|
||||||
label: "大小",
|
pagination={pagination}
|
||||||
value: knowledgeBase.size,
|
scroll={{ y: "calc(100vh - 30rem)" }}
|
||||||
},
|
|
||||||
]}
|
|
||||||
operations={[
|
|
||||||
{
|
|
||||||
key: "edit",
|
|
||||||
label: "修改参数配置",
|
|
||||||
icon: <Edit className="w-4 h-4" />,
|
|
||||||
onClick: () => {
|
|
||||||
setEditForm(knowledgeBase);
|
|
||||||
setCurrentView("config");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "vector",
|
|
||||||
label: "向量化管理",
|
|
||||||
icon: <VectorSquareIcon className="w-4 h-4" />,
|
|
||||||
onClick: () => setShowVectorizationDialog(true),
|
|
||||||
},
|
|
||||||
...(knowledgeBase.status === "error"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: "retry",
|
|
||||||
label: "重试",
|
|
||||||
onClick: () => {}, // 填写重试逻辑
|
|
||||||
danger: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
key: "more",
|
|
||||||
label: "更多操作",
|
|
||||||
icon: <MoreHorizontal className="w-4 h-4" />,
|
|
||||||
isDropdown: true,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
key: "download",
|
|
||||||
label: "导出",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "settings",
|
|
||||||
label: "配置",
|
|
||||||
},
|
|
||||||
{ type: "divider" },
|
|
||||||
{
|
|
||||||
key: "delete",
|
|
||||||
label: "删除",
|
|
||||||
danger: true,
|
|
||||||
onClick: () => handleDeleteKB(knowledgeBase),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
{/* Tab Navigation */}
|
|
||||||
<Card>
|
|
||||||
{/* Files Section */}
|
|
||||||
<div className="flex items-center justify-between mb-4 gap-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<SearchControls
|
|
||||||
searchTerm={fileSearchQuery}
|
|
||||||
onSearchChange={setFileSearchQuery}
|
|
||||||
searchPlaceholder="搜索文件名..."
|
|
||||||
filters={[
|
|
||||||
{
|
|
||||||
key: "status",
|
|
||||||
label: "状态筛选",
|
|
||||||
options: [
|
|
||||||
{ label: "全部状态", value: "all" },
|
|
||||||
{ label: "已完成", value: "completed" },
|
|
||||||
{ label: "处理中", value: "processing" },
|
|
||||||
{ label: "向量化中", value: "vectorizing" },
|
|
||||||
{ label: "错误", value: "error" },
|
|
||||||
{ label: "已禁用", value: "disabled" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onFiltersChange={(filters) => {
|
|
||||||
setFileStatusFilter(filters.status?.[0] || "all");
|
|
||||||
}}
|
|
||||||
showViewToggle={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button type="primary">
|
|
||||||
<Plus className="w-4 h-4 mr-1" />
|
|
||||||
添加文件
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Files Table */}
|
|
||||||
<Table
|
|
||||||
columns={fileColumns}
|
|
||||||
dataSource={files}
|
|
||||||
rowKey="id"
|
|
||||||
pagination={false}
|
|
||||||
locale={{
|
|
||||||
emptyText: (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<File className="w-12 h-12 mx-auto mb-4 text-gray-300" />
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
||||||
没有找到文件
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500 mb-4">
|
|
||||||
尝试调整搜索条件或添加新文件
|
|
||||||
</p>
|
|
||||||
<Button type="dashed">
|
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
|
||||||
添加文件
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Vectorization Dialog */}
|
|
||||||
<Modal
|
|
||||||
open={showVectorizationDialog}
|
|
||||||
onCancel={() => setShowVectorizationDialog(false)}
|
|
||||||
footer={null}
|
|
||||||
title="向量化管理"
|
|
||||||
width={700}
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<Card className="p-4">
|
|
||||||
<h4 className="font-medium mb-2">当前状态</h4>
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>已向量化文件:</span>
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
knowledgeBase.files.filter(
|
|
||||||
(f) => f.vectorizationStatus === "completed"
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
/{knowledgeBase.files.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>向量总数:</span>
|
|
||||||
<span>
|
|
||||||
{knowledgeBase.vectorCount?.toLocaleString?.() ?? 0}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>存储大小:</span>
|
|
||||||
<span>{knowledgeBase.size}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card className="p-4">
|
|
||||||
<h4 className="font-medium mb-2">操作选项</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => handleStartVectorization()}
|
|
||||||
>
|
|
||||||
<Zap className="w-4 h-4 mr-2" />
|
|
||||||
批量向量化
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => message.info("TODO: 重新向量化全部")}
|
|
||||||
>
|
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
|
||||||
重新向量化全部
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
danger
|
|
||||||
onClick={() => message.info("TODO: 清空向量数据")}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
|
||||||
清空向量数据
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-3">文件向量化状态</h4>
|
|
||||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
|
||||||
{knowledgeBase.files.map((file: KBFile) => (
|
|
||||||
<div
|
|
||||||
key={file.id}
|
|
||||||
className="flex items-center justify-between p-3 border rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<File className="w-4 h-4 text-gray-400" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-sm">{file.name}</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
{file.chunkCount} 个分块
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
status={getStatusBadgeVariant(
|
|
||||||
file.vectorizationStatus || "pending"
|
|
||||||
)}
|
|
||||||
text={getStatusLabel(
|
|
||||||
file.vectorizationStatus || "pending"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{file.vectorizationStatus === "processing" && (
|
|
||||||
<div className="w-16">
|
|
||||||
<Progress
|
|
||||||
percent={file.progress}
|
|
||||||
size="small"
|
|
||||||
showInfo={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{file.vectorizationStatus !== "completed" && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() => handleStartVectorization(file.id)}
|
|
||||||
>
|
|
||||||
<StarOff className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end pt-2">
|
|
||||||
<Button onClick={() => setShowVectorizationDialog(false)}>
|
|
||||||
关闭
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
{/* Edit File Dialog */}
|
|
||||||
<Modal
|
|
||||||
open={!!showEditFileDialog}
|
|
||||||
onCancel={() => setShowEditFileDialog(null)}
|
|
||||||
title="编辑文件"
|
|
||||||
width={600}
|
|
||||||
footer={[
|
|
||||||
<Button key="cancel" onClick={() => setShowEditFileDialog(null)}>
|
|
||||||
取消
|
|
||||||
</Button>,
|
|
||||||
<Button
|
|
||||||
key="save"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => setShowEditFileDialog(null)}
|
|
||||||
>
|
|
||||||
<Save className="w-4 h-4 mr-2" />
|
|
||||||
保存更改
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block mb-1">文件名</label>
|
|
||||||
<Input value={showEditFileDialog?.name} readOnly />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block mb-1">文件来源</label>
|
|
||||||
<Input
|
|
||||||
value={
|
|
||||||
showEditFileDialog?.source === "upload"
|
|
||||||
? "上传文件"
|
|
||||||
: "数据集文件"
|
|
||||||
}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showEditFileDialog?.source === "upload" ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<label className="block mb-1">更新文件</label>
|
|
||||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
|
|
||||||
<Upload className="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
拖拽或点击上传新版本文件
|
|
||||||
</p>
|
|
||||||
<Button className="mt-2 bg-transparent" disabled>
|
|
||||||
选择文件
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<label className="block mb-1">数据集文件管理</label>
|
|
||||||
<div className="p-4 border rounded-lg bg-gray-50">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
当前数据集: {showEditFileDialog?.datasetId}
|
|
||||||
</span>
|
|
||||||
<Button size="small">
|
|
||||||
<RefreshCw className="w-4 h-4 mr-1" />
|
|
||||||
更新数据集文件
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-600">
|
|
||||||
此文件来自数据集,可以选择更新数据集中的对应文件或切换到其他数据集文件
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<label className="block mb-1">处理选项</label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox id="reprocess" />
|
|
||||||
<span className="text-sm">更新后重新处理分块</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox id="revectorize" />
|
|
||||||
<span className="text-sm">重新生成向量</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
195
frontend/src/pages/KnowledgeBase/Home/KnowledgeBasePage.tsx
Normal file
195
frontend/src/pages/KnowledgeBase/Home/KnowledgeBasePage.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
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,
|
||||||
|
} = useFetchData<KnowledgeBaseItem>(
|
||||||
|
queryKnowledgeBasesUsingPost,
|
||||||
|
mapKnowledgeBase
|
||||||
|
);
|
||||||
|
|
||||||
|
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: "embeddingModel",
|
||||||
|
key: "embeddingModel",
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "大语言模型",
|
||||||
|
dataIndex: "chatModel",
|
||||||
|
key: "chatModel",
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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={(keyword) =>
|
||||||
|
setSearchParams({ ...searchParams, keyword })
|
||||||
|
}
|
||||||
|
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,65 +1,320 @@
|
|||||||
export default function AddDataDialog() {
|
import { useEffect, useState } from "react";
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
import {
|
||||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
Button,
|
||||||
const { message } = App.useApp();
|
App,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Form,
|
||||||
|
Modal,
|
||||||
|
UploadFile,
|
||||||
|
Radio,
|
||||||
|
Tree,
|
||||||
|
} from "antd";
|
||||||
|
import { InboxOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
|
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||||
|
import Dragger from "antd/es/upload/Dragger";
|
||||||
|
import {
|
||||||
|
queryDatasetFilesUsingGet,
|
||||||
|
queryDatasetsUsingGet,
|
||||||
|
} from "@/pages/DataManagement/dataset.api";
|
||||||
|
import { datasetTypeMap } from "@/pages/DataManagement/dataset.const";
|
||||||
|
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
||||||
|
import { DatasetType } from "@/pages/DataManagement/dataset.model";
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const dataSourceOptions = [
|
||||||
if (e.target.files) {
|
{ label: "本地上传", value: "local" },
|
||||||
setSelectedFiles(Array.from(e.target.files));
|
{ label: "数据集", value: "dataset" },
|
||||||
}
|
];
|
||||||
|
|
||||||
|
const sliceOptions = [
|
||||||
|
{ label: "章节分块", value: "CHAPTER_CHUNK" },
|
||||||
|
{ label: "段落分块", value: "PARAGRAPH_CHUNK" },
|
||||||
|
{ label: "长度分块", value: "LENGTH_CHUNK" },
|
||||||
|
{ label: "自定义分割符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||||
|
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
dataIndex: "name",
|
||||||
|
title: "名称",
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "datasetType",
|
||||||
|
title: "类型",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (type) => datasetTypeMap[type].label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "size",
|
||||||
|
title: "大小",
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "fileCount",
|
||||||
|
title: "文件数",
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function AddDataDialog({ knowledgeBase }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
|
||||||
|
// Form initial values
|
||||||
|
const [newKB, setNewKB] = useState<Partial<KnowledgeBaseItem>>({
|
||||||
|
dataSource: "dataset",
|
||||||
|
processType: "DEFAULT_CHUNK",
|
||||||
|
chunkSize: 500,
|
||||||
|
overlap: 50,
|
||||||
|
datasetIds: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [filesTree, setFilesTree] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const fetchDatasets = async () => {
|
||||||
|
const { data } = await queryDatasetsUsingGet({
|
||||||
|
page: 0,
|
||||||
|
size: 1000,
|
||||||
|
type: DatasetType.TEXT,
|
||||||
|
});
|
||||||
|
const datasets =
|
||||||
|
data.content.map((item) => ({
|
||||||
|
...item,
|
||||||
|
key: item.id,
|
||||||
|
title: item.name,
|
||||||
|
isLeaf: item.fileCount === 0,
|
||||||
|
disabled: item.fileCount === 0,
|
||||||
|
})) || [];
|
||||||
|
setFilesTree(datasets);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
useEffect(() => {
|
||||||
if (selectedFiles.length === 0) {
|
if (isOpen) fetchDatasets();
|
||||||
message.error("请先选择文件");
|
}, [isOpen]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const updateTreeData = (list, key: React.Key, children) =>
|
||||||
const formData = new FormData();
|
list.map((node) => {
|
||||||
selectedFiles.forEach((file) => {
|
if (node.key === key) {
|
||||||
formData.append("files", file);
|
return {
|
||||||
|
...node,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (node.children) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
children: updateTreeData(node.children, key, children),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onLoadFiles = async ({ key, children }) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
if (children) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryDatasetFilesUsingGet(key, {
|
||||||
|
page: 0,
|
||||||
|
size: 1000,
|
||||||
|
}).then(({ data }) => {
|
||||||
|
const children = data.content.map((file) => ({
|
||||||
|
title: file.fileName,
|
||||||
|
key: file.id,
|
||||||
|
isLeaf: true,
|
||||||
|
}));
|
||||||
|
setFilesTree((origin) => updateTreeData(origin, key, children));
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
await uploadDataFilesUsingPost(formData);
|
const handleBeforeUpload = (_, files: UploadFile[]) => {
|
||||||
message.success("文件上传成功");
|
setFileList([...fileList, ...files]);
|
||||||
setIsOpen(false);
|
return false;
|
||||||
setSelectedFiles([]);
|
};
|
||||||
} catch (error) {
|
|
||||||
message.error("文件上传失败");
|
const handleRemoveFile = (file: UploadFile) => {
|
||||||
}
|
setFileList((prev) => prev.filter((f) => f.uid !== file.uid));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddData = async () => {
|
||||||
|
await addKnowledgeBaseFilesUsingPost(knowledgeBase.id, {
|
||||||
|
knowledgeBaseId: knowledgeBase.id,
|
||||||
|
files: newKB.dataSource === "local" ? fileList : newKB.files,
|
||||||
|
processType: newKB.processType,
|
||||||
|
chunkSize: newKB.chunkSize,
|
||||||
|
overlap: newKB.overlap,
|
||||||
|
delimiter: newKB.delimiter,
|
||||||
|
});
|
||||||
|
message.success("数据添加成功");
|
||||||
|
form.resetFields();
|
||||||
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button type="primary" onClick={() => setIsOpen(true)}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
>
|
||||||
添加数据
|
添加数据
|
||||||
</Button>
|
</Button>
|
||||||
<Modal
|
<Modal
|
||||||
title="添加数据文件"
|
title="添加数据"
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onCancel={() => setIsOpen(false)}
|
onCancel={() => setIsOpen(false)}
|
||||||
onOk={handleUpload}
|
onOk={handleAddData}
|
||||||
okText="上传"
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
width={1000}
|
||||||
>
|
>
|
||||||
<input
|
<div className="overflow-auto p-6">
|
||||||
type="file"
|
<Form
|
||||||
multiple
|
form={form}
|
||||||
onChange={handleFileChange}
|
layout="vertical"
|
||||||
accept=".txt,.pdf,.docx,.csv,.json"
|
initialValues={newKB}
|
||||||
/>
|
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
||||||
{selectedFiles.length > 0 && (
|
>
|
||||||
<div className="mt-4">
|
<Form.Item
|
||||||
<h4>已选择的文件:</h4>
|
label="分块方式"
|
||||||
<ul>
|
name="processType"
|
||||||
{selectedFiles.map((file, index) => (
|
required
|
||||||
<li key={index}>
|
rules={[{ required: true }]}
|
||||||
{file.name} - {(file.size / 1024).toFixed(2)} KB
|
>
|
||||||
</li>
|
<Select options={sliceOptions}></Select>
|
||||||
))}
|
</Form.Item>
|
||||||
</ul>
|
|
||||||
</div>
|
<div className="grid grid-cols-2 gap-3">
|
||||||
)}
|
<Form.Item
|
||||||
|
label="分块大小"
|
||||||
|
name="chunkSize"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入分块大小",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="重叠长度"
|
||||||
|
name="overlap"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入重叠长度",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
{newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && (
|
||||||
|
<Form.Item
|
||||||
|
label="分隔符"
|
||||||
|
name="delimiter"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入分隔符",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="输入分隔符,如 \\n\\n" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
label="数据来源"
|
||||||
|
name="dataSource"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请选择数据来源",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Radio.Group options={dataSourceOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
{newKB.dataSource === "local" && (
|
||||||
|
<Form.Item
|
||||||
|
label="上传文件"
|
||||||
|
name="files"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请上传文件",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Dragger
|
||||||
|
className="w-full"
|
||||||
|
onRemove={handleRemoveFile}
|
||||||
|
beforeUpload={handleBeforeUpload}
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<InboxOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">本地文件上传</p>
|
||||||
|
<p className="ant-upload-hint">
|
||||||
|
拖拽文件到此处或点击选择文件
|
||||||
|
</p>
|
||||||
|
</Dragger>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{newKB.dataSource === "dataset" && (
|
||||||
|
<Form.Item
|
||||||
|
label="选择数据集文件"
|
||||||
|
name="datasetId"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请选择数据集",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="border-card p-4 overflow-auto h-[300px]">
|
||||||
|
<Tree
|
||||||
|
blockNode
|
||||||
|
multiple
|
||||||
|
loadData={onLoadFiles}
|
||||||
|
treeData={filesTree}
|
||||||
|
onSelect={(_, { selectedNodes }) => {
|
||||||
|
console.log({
|
||||||
|
...newKB,
|
||||||
|
files: selectedNodes
|
||||||
|
.filter((node) => node.isLeaf)
|
||||||
|
.map((node) => ({
|
||||||
|
...node,
|
||||||
|
id: node.key,
|
||||||
|
name: node.title,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewKB({
|
||||||
|
...newKB,
|
||||||
|
files: selectedNodes
|
||||||
|
.filter((node) => node.isLeaf)
|
||||||
|
.map((node) => ({
|
||||||
|
...node,
|
||||||
|
id: node.key,
|
||||||
|
name: node.title,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,11 +12,15 @@ import { KnowledgeBaseItem } from "../knowledge-base.model";
|
|||||||
export default function CreateKnowledgeBase({
|
export default function CreateKnowledgeBase({
|
||||||
isEdit,
|
isEdit,
|
||||||
data,
|
data,
|
||||||
|
showBtn = true,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
isEdit?: boolean;
|
isEdit?: boolean;
|
||||||
|
showBtn?: boolean;
|
||||||
data?: Partial<KnowledgeBaseItem> | null;
|
data?: Partial<KnowledgeBaseItem> | null;
|
||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -74,24 +78,32 @@ export default function CreateKnowledgeBase({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setOpen(false);
|
||||||
|
onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
{showBtn && (
|
||||||
type="primary"
|
<Button
|
||||||
icon={<PlusOutlined />}
|
type="primary"
|
||||||
onClick={() => {
|
icon={<PlusOutlined />}
|
||||||
form.resetFields();
|
onClick={() => {
|
||||||
setOpen(true);
|
form.resetFields();
|
||||||
}}
|
setOpen(true);
|
||||||
>
|
}}
|
||||||
创建知识库
|
>
|
||||||
</Button>
|
创建知识库
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Modal
|
<Modal
|
||||||
title={isEdit ? "编辑知识库" : "创建知识库"}
|
title={isEdit ? "编辑知识库" : "创建知识库"}
|
||||||
open={open}
|
open={open}
|
||||||
okText="确定"
|
okText="确定"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
onCancel={() => setOpen(false)}
|
maskClosable={false}
|
||||||
|
onCancel={handleCloseModal}
|
||||||
onOk={handleCreateKnowledgeBase}
|
onOk={handleCreateKnowledgeBase}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, Transfer } from "antd";
|
||||||
|
import type {
|
||||||
|
GetProp,
|
||||||
|
TableColumnsType,
|
||||||
|
TableProps,
|
||||||
|
TransferProps,
|
||||||
|
} from "antd";
|
||||||
|
|
||||||
|
type TransferItem = GetProp<TransferProps, "dataSource">[number];
|
||||||
|
type TableRowSelection<T extends object> = TableProps<T>["rowSelection"];
|
||||||
|
|
||||||
|
interface DataType {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableTransferProps extends TransferProps<TransferItem> {
|
||||||
|
dataSource: DataType[];
|
||||||
|
leftColumns: TableColumnsType<DataType>;
|
||||||
|
rightColumns: TableColumnsType<DataType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customize Table Transfer
|
||||||
|
const TableTransfer: React.FC<TableTransferProps> = (props) => {
|
||||||
|
const { leftColumns, rightColumns, ...restProps } = props;
|
||||||
|
return (
|
||||||
|
<Transfer style={{ width: "100%" }} {...restProps}>
|
||||||
|
{({
|
||||||
|
direction,
|
||||||
|
filteredItems,
|
||||||
|
onItemSelect,
|
||||||
|
onItemSelectAll,
|
||||||
|
selectedKeys: listSelectedKeys,
|
||||||
|
disabled: listDisabled,
|
||||||
|
}) => {
|
||||||
|
const columns = direction === "left" ? leftColumns : rightColumns;
|
||||||
|
const rowSelection: TableRowSelection<TransferItem> = {
|
||||||
|
getCheckboxProps: () => ({ disabled: listDisabled }),
|
||||||
|
onChange(selectedRowKeys) {
|
||||||
|
onItemSelectAll(selectedRowKeys, "replace");
|
||||||
|
},
|
||||||
|
selectedRowKeys: listSelectedKeys,
|
||||||
|
selections: [
|
||||||
|
Table.SELECTION_ALL,
|
||||||
|
Table.SELECTION_INVERT,
|
||||||
|
Table.SELECTION_NONE,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredItems}
|
||||||
|
size="small"
|
||||||
|
scroll={{ y: 300 }}
|
||||||
|
style={{ pointerEvents: listDisabled ? "none" : undefined }}
|
||||||
|
onRow={({ key, disabled: itemDisabled }) => ({
|
||||||
|
onClick: () => {
|
||||||
|
if (itemDisabled || listDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onItemSelect(key, !listSelectedKeys.includes(key));
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Transfer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableTransfer;
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { get, post, put, del } from "@/utils/request";
|
import { get, post, put, del } from "@/utils/request";
|
||||||
|
|
||||||
|
|
||||||
// 获取知识库列表
|
// 获取知识库列表
|
||||||
export function queryKnowledgeBasesUsingPost(params: any) {
|
export function queryKnowledgeBasesUsingPost(params: any) {
|
||||||
console.log('get tk', params);
|
console.log("get tk", params);
|
||||||
|
|
||||||
return post("/api/knowledge-base/list", params);
|
return post("/api/knowledge-base/list", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,22 +27,28 @@ export function deleteKnowledgeBaseByIdUsingDelete(baseId: string) {
|
|||||||
return del(`/api/knowledge-base/${baseId}`);
|
return del(`/api/knowledge-base/${baseId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取知识生成任务列表
|
// 获取知识生成文件列表
|
||||||
export function queryKnowledgeGenerationTasksUsingPost(params: any) {
|
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data) {
|
||||||
return post("/api/knowledge-base/tasks", params);
|
return get(`/api/knowledge-base/${baseId}/files`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加文件到知识库
|
// 添加文件到知识库
|
||||||
export function addKnowledgeGenerationFilesUsingPost(baseId: string, data: any) {
|
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: any) {
|
||||||
return post(`/api/knowledge-base/${baseId}/files`, data);
|
return post(`/api/knowledge-base/${baseId}/files`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取知识生成文件详情
|
// 获取知识生成文件详情
|
||||||
export function queryKnowledgeGenerationFilesByIdUsingGet(baseId: string, fileId: string) {
|
export function queryKnowledgeBaseFilesByIdUsingGet(
|
||||||
|
baseId: string,
|
||||||
|
fileId: string
|
||||||
|
) {
|
||||||
return get(`/api/knowledge-base/${baseId}/files/${fileId}`);
|
return get(`/api/knowledge-base/${baseId}/files/${fileId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除知识生成文件
|
// 删除知识生成文件
|
||||||
export function deleteKnowledgeGenerationTaskByIdUsingDelete(baseId: string) {
|
export function deleteKnowledgeBaseFileByIdUsingDelete(
|
||||||
return del(`/api/knowledge-base/${baseId}/files`);
|
baseId: string,
|
||||||
|
fileId: string
|
||||||
|
) {
|
||||||
|
return del(`/api/knowledge-base/${baseId}/files/${fileId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,48 @@
|
|||||||
import {
|
import {
|
||||||
BookOpen,
|
BookOpen,
|
||||||
BookOpenText,
|
BookOpenText,
|
||||||
|
BookType,
|
||||||
|
ChartNoAxesColumn,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
CircleEllipsis,
|
||||||
Clock,
|
Clock,
|
||||||
Database,
|
Database,
|
||||||
|
File,
|
||||||
|
VectorSquare,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { KBStatus, KBType, KnowledgeBaseItem } from "./knowledge-base.model";
|
import {
|
||||||
|
KBFile,
|
||||||
|
KBFileStatus,
|
||||||
|
KBType,
|
||||||
|
KnowledgeBaseItem,
|
||||||
|
} from "./knowledge-base.model";
|
||||||
import { formatBytes, formatDateTime, formatNumber } from "@/utils/unit";
|
import { formatBytes, formatDateTime, formatNumber } from "@/utils/unit";
|
||||||
|
|
||||||
export const KBStatusMap = {
|
export const KBFileStatusMap = {
|
||||||
[KBStatus.READY]: {
|
[KBFileStatus.PROCESSED]: {
|
||||||
label: KBStatus.READY,
|
value: KBFileStatus.PROCESSED,
|
||||||
|
label: "已处理",
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
color: "#389e0d",
|
color: "#389e0d",
|
||||||
},
|
},
|
||||||
[KBStatus.VECTORIZING]: {
|
[KBFileStatus.PROCESSING]: {
|
||||||
label: KBStatus.PROCESSING,
|
value: KBFileStatus.PROCESSING,
|
||||||
|
label: "处理中",
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
color: "#3b82f6",
|
color: "#faad14",
|
||||||
},
|
},
|
||||||
[KBStatus.ERROR]: {
|
[KBFileStatus.PROCESS_FAILED]: {
|
||||||
label: KBStatus.ERROR,
|
value: KBFileStatus.PROCESS_FAILED,
|
||||||
|
label: "处理失败",
|
||||||
icon: XCircle,
|
icon: XCircle,
|
||||||
color: "#ef4444",
|
color: "#ff4d4f",
|
||||||
|
},
|
||||||
|
[KBFileStatus.UNPROCESSED]: {
|
||||||
|
value: KBFileStatus.UNPROCESSED,
|
||||||
|
label: "未处理",
|
||||||
|
icon: CircleEllipsis,
|
||||||
|
color: "#d9d9d9",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,12 +69,47 @@ export function mapKnowledgeBase(kb: KnowledgeBaseItem): KnowledgeBaseItem {
|
|||||||
icon: <BookOpenText className="text-gray-400" />,
|
icon: <BookOpenText className="text-gray-400" />,
|
||||||
description: kb.description,
|
description: kb.description,
|
||||||
statistics: [
|
statistics: [
|
||||||
{ label: "索引模型", value: kb.embeddingModel },
|
{
|
||||||
{ label: "文本理解模型", value: kb.chatModel },
|
label: "索引模型",
|
||||||
{ label: "文件数", value: formatNumber(kb?.fileCount) || 0 },
|
key: "embeddingModel",
|
||||||
{ label: "大小", value: formatBytes(kb?.size) || "0 MB" },
|
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
|
||||||
|
value: kb.embeddingModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "文本理解模型",
|
||||||
|
key: "chatModel",
|
||||||
|
icon: <BookType className="w-4 h-4 text-green-500" />,
|
||||||
|
value: kb.chatModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "文件数",
|
||||||
|
key: "fileCount",
|
||||||
|
icon: <File className="w-4 h-4 text-yellow-500" />,
|
||||||
|
value: formatNumber(kb?.fileCount) || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "大小",
|
||||||
|
key: "size",
|
||||||
|
icon: <ChartNoAxesColumn className="w-4 h-4 text-red-500" />,
|
||||||
|
value: formatBytes(kb?.size) || "0 MB",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
updatedAt: formatDateTime(kb.updatedAt),
|
updatedAt: formatDateTime(kb.updatedAt),
|
||||||
createdAt: formatDateTime(kb.createdAt),
|
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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
export enum KBStatus {
|
export enum KBFileStatus {
|
||||||
READY = "ready",
|
UNPROCESSED = "UNPROCESSED",
|
||||||
PROCESSING = "processing",
|
PROCESSING = "PROCESSING",
|
||||||
VECTORIZING = "vectorizing",
|
PROCESSED = "PROCESSED",
|
||||||
IMPORTING = "importing",
|
PROCESS_FAILED = "PROCESS_FAILED",
|
||||||
ERROR = "error",
|
|
||||||
DISABLED = "disabled",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum KBType {
|
export enum KBType {
|
||||||
@@ -25,17 +23,17 @@ export interface KnowledgeBaseItem {
|
|||||||
|
|
||||||
export interface KBFile {
|
export interface KBFile {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
fileName: string;
|
||||||
type: string;
|
name?: string;
|
||||||
size: string;
|
createdAt: string;
|
||||||
status: "processing" | "completed" | "error" | "disabled" | "vectorizing";
|
updatedAt: string;
|
||||||
|
status: KBFileStatus;
|
||||||
chunkCount: number;
|
chunkCount: number;
|
||||||
progress: number;
|
metadata: Record<string, any>;
|
||||||
uploadedAt: string;
|
knowledgeBaseId: string;
|
||||||
source: "upload" | "dataset";
|
fileId: string;
|
||||||
datasetId?: string;
|
updatedBy: string;
|
||||||
chunks?: Chunk[];
|
createdBy: string;
|
||||||
vectorizationStatus?: "pending" | "processing" | "completed" | "failed";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Chunk {
|
interface Chunk {
|
||||||
@@ -74,12 +72,3 @@ interface VectorizationRecord {
|
|||||||
};
|
};
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SliceOperator {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
type: "text" | "semantic" | "structure" | "custom";
|
|
||||||
icon: string;
|
|
||||||
params: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function TaskUpload() {
|
|||||||
></Button>
|
></Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Progress size="small" percent={Number(task.percent.toFixed(2))} />
|
<Progress size="small" percent={task.percent} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{taskList.length === 0 && (
|
{taskList.length === 0 && (
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import EvaluationTaskCreate from "@/pages/DataEvaluation/Create/CreateTask";
|
|||||||
import EvaluationTaskReport from "@/pages/DataEvaluation/Report/EvaluationReport";
|
import EvaluationTaskReport from "@/pages/DataEvaluation/Report/EvaluationReport";
|
||||||
import ManualEvaluatePage from "@/pages/DataEvaluation/Evaluate/ManualEvaluate";
|
import ManualEvaluatePage from "@/pages/DataEvaluation/Evaluate/ManualEvaluate";
|
||||||
|
|
||||||
import KnowledgeGenerationPage from "@/pages/KnowledgeBase/Home/KnowledgeGeneration";
|
import KnowledgeBasePage from "@/pages/KnowledgeBase/Home/KnowledgeBasePage";
|
||||||
import KnowledgeBaseDetailPage from "@/pages/KnowledgeBase/Detail/KnowledgeBaseDetail";
|
import KnowledgeBaseDetailPage from "@/pages/KnowledgeBase/Detail/KnowledgeBaseDetail";
|
||||||
import KnowledgeBaseFileDetailPage from "@/pages/KnowledgeBase/FileDetail/KnowledgeBaseFileDetail";
|
import KnowledgeBaseFileDetailPage from "@/pages/KnowledgeBase/FileDetail/KnowledgeBaseFileDetail";
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
index: true,
|
index: true,
|
||||||
Component: KnowledgeGenerationPage,
|
Component: KnowledgeBasePage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "detail/:id",
|
path: "detail/:id",
|
||||||
|
|||||||
Reference in New Issue
Block a user