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>({
|
||||
data,
|
||||
data = {} as T,
|
||||
statistics,
|
||||
operations,
|
||||
tagConfig,
|
||||
@@ -59,7 +59,7 @@ function DetailHeader<T>({
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<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 && (
|
||||
<Tag color={data.status?.color}>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
@@ -86,7 +86,7 @@ function DetailHeader<T>({
|
||||
)}
|
||||
</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">
|
||||
{statistics.map((stat) => (
|
||||
<div key={stat.key} className="flex items-center gap-1">
|
||||
@@ -112,13 +112,10 @@ function DetailHeader<T>({
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Popconfirm
|
||||
key={op.key}
|
||||
title={op.confirm.title}
|
||||
description={op.confirm.description}
|
||||
{...op.confirm}
|
||||
onConfirm={() => {
|
||||
op?.onClick();
|
||||
op?.confirm?.onConfirm?.();
|
||||
}}
|
||||
okText={op.confirm.okText || "确定"}
|
||||
cancelText={op.confirm.cancelText || "取消"}
|
||||
okType={op.danger ? "danger" : "primary"}
|
||||
overlayStyle={{ zIndex: 9999 }}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function useFetchData<T>(
|
||||
fetchFunc: (params?: any) => Promise<any>,
|
||||
mapDataFunc: (data: Partial<T>) => T = (data) => data as T,
|
||||
pollingInterval: number = 30000, // 默认30秒轮询一次
|
||||
autoRefresh: boolean = true,
|
||||
autoRefresh: boolean = false, // 是否自动开始轮询,默认 false
|
||||
additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数
|
||||
pageOffset: number = 1
|
||||
) {
|
||||
|
||||
@@ -120,10 +120,11 @@ const MockAPI = {
|
||||
queryKnowledgeBaseByIdUsingGet: "/knowledge-base/:baseId", // 根据ID获取知识库详情
|
||||
updateKnowledgeBaseByIdUsingPut: "/knowledge-base/:baseId", // 更新知识库
|
||||
deleteKnowledgeBaseByIdUsingDelete: "/knowledge-base/:baseId", // 删除知识库
|
||||
queryKnowledgeGenerationTasksUsingPost: "/knowledge-base/tasks", // 获取知识生成任务列表
|
||||
addKnowledgeGenerationFilesUsingPost: "/knowledge-base/:baseId/files", // 添加文件到知识库
|
||||
queryKnowledgeGenerationFilesByIdUsingGet: "/knowledge-base/:baseId/files/:fileId", // 根据ID获取知识生成文件详情
|
||||
deleteKnowledgeGenerationTaskByIdUsingDelete: "/knowledge-base/:baseId/files", // 删除知识生成文件
|
||||
addKnowledgeBaseFilesUsingPost: "/knowledge-base/:baseId/files", // 添加文件到知识库
|
||||
queryKnowledgeBaseFilesGet: "/knowledge-base/:baseId/files", // 根据ID获取知识生成文件列表
|
||||
queryKnowledgeBaseFilesByIdUsingGet:
|
||||
"/knowledge-base/:baseId/files/:fileId", // 根据ID获取知识生成文件详情
|
||||
deleteKnowledgeBaseTaskByIdUsingDelete: "/knowledge-base/:baseId/files/:id", // 删除知识生成文件
|
||||
|
||||
// 算子市场
|
||||
queryOperatorsUsingPost: "/operators/list", // 获取算子列表
|
||||
|
||||
@@ -161,8 +161,6 @@ module.exports = function (router) {
|
||||
);
|
||||
}
|
||||
if (type) {
|
||||
console.log("filter type:", type);
|
||||
|
||||
filteredDatasets = filteredDatasets.filter(
|
||||
(dataset) => dataset.datasetType === type
|
||||
);
|
||||
|
||||
@@ -22,6 +22,29 @@ function 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) {
|
||||
// 获取知识库列表
|
||||
router.post(API.queryKnowledgeBasesUsingPost, (req, res) => {
|
||||
@@ -56,15 +79,16 @@ module.exports = function (router) {
|
||||
});
|
||||
|
||||
// 获取知识库详情
|
||||
router.get(
|
||||
new RegExp(API.queryKnowledgeBaseByIdUsingGet.replace(":baseId", "(\\w+)")),
|
||||
(req, res) => {
|
||||
const id = req.params.baseId;
|
||||
const item =
|
||||
knowledgeBaseList.find((kb) => kb.id === id) || KnowledgeBaseItem();
|
||||
res.send(item);
|
||||
}
|
||||
);
|
||||
router.get(API.queryKnowledgeBaseByIdUsingGet, (req, res) => {
|
||||
const id = req.params.baseId;
|
||||
const item =
|
||||
knowledgeBaseList.find((kb) => kb.id === id) || KnowledgeBaseItem();
|
||||
res.send({
|
||||
code: "0",
|
||||
msg: "Success",
|
||||
data: item,
|
||||
});
|
||||
});
|
||||
|
||||
// 更新知识库
|
||||
router.put(API.updateKnowledgeBaseByIdUsingPut, (req, res) => {
|
||||
@@ -90,72 +114,63 @@ module.exports = function (router) {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取知识生成任务列表
|
||||
router.post(API.queryKnowledgeGenerationTasksUsingPost, (req, res) => {
|
||||
const tasks = Mock.mock({
|
||||
"data|10": [
|
||||
{
|
||||
id: "@guid",
|
||||
name: "@ctitle(5,15)",
|
||||
status: '@pick(["pending","running","success","failed"])',
|
||||
createdAt: "@datetime",
|
||||
updatedAt: "@datetime",
|
||||
progress: "@integer(0,100)",
|
||||
},
|
||||
],
|
||||
total: 10,
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
// 添加文件到知识库
|
||||
router.post(API.addKnowledgeBaseFilesUsingPost, (req, res) => {
|
||||
const file = Mock.mock({
|
||||
id: "@guid",
|
||||
name: "@ctitle(5,15)",
|
||||
size: "@integer(1000,1000000)",
|
||||
status: "uploaded",
|
||||
createdAt: "@datetime",
|
||||
});
|
||||
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(
|
||||
new RegExp(
|
||||
API.queryKnowledgeGenerationFilesByIdUsingGet
|
||||
.replace(":baseId", "(\\w+)")
|
||||
.replace(":fileId", "(\\w+)")
|
||||
),
|
||||
(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);
|
||||
router.get(API.queryKnowledgeBaseFilesGet, (req, res) => {
|
||||
const { keyword, page, size } = req.query;
|
||||
let filteredList = fileList;
|
||||
if (keyword) {
|
||||
filteredList = fileList.filter((file) => file.fileName.includes(keyword));
|
||||
}
|
||||
);
|
||||
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(
|
||||
new RegExp(
|
||||
API.deleteKnowledgeGenerationTaskByIdUsingDelete.replace(
|
||||
":baseId",
|
||||
"(\\w+)"
|
||||
)
|
||||
),
|
||||
(req, res) => {
|
||||
res.send({ success: true });
|
||||
router.delete(API.deleteKnowledgeBaseTaskByIdUsingDelete, (req, res) => {
|
||||
const { id } = req.params;
|
||||
const idx = fileList.findIndex((file) => file.id === id);
|
||||
if (idx >= 0) {
|
||||
fileList.splice(idx, 1);
|
||||
res.status(200).send({ success: true });
|
||||
return;
|
||||
}
|
||||
);
|
||||
res.status(404).send({ message: "Not found" });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -200,6 +200,7 @@ export function mapDataset(dataset: AnyObject): Dataset {
|
||||
datasetTypeMap[dataset?.datasetType] || {};
|
||||
return {
|
||||
...dataset,
|
||||
key: dataset.id,
|
||||
type: datasetTypeMap[dataset.datasetType]?.label || "未知",
|
||||
size: formatBytes(dataset.totalSize || 0),
|
||||
createdAt: formatDateTime(dataset.createdAt) || "--",
|
||||
|
||||
@@ -1,628 +1,236 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Table, Badge, Button, Breadcrumb, Tooltip, App } from "antd";
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
File,
|
||||
Trash2,
|
||||
Save,
|
||||
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";
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
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 navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
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
|
||||
const handleDeleteFile = (file: KBFile) => {};
|
||||
|
||||
|
||||
const handleDeleteKB = (kb: KnowledgeBase) => {};
|
||||
|
||||
// 状态 Badge 映射
|
||||
function getStatusBadgeVariant(status: string) {
|
||||
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";
|
||||
const handleDeleteFile = async (file: KBFile) => {
|
||||
try {
|
||||
await deleteKnowledgeBaseFileByIdUsingDelete(knowledgeBase.id, file.id);
|
||||
message.success("文件已删除");
|
||||
fetchFiles();
|
||||
} catch (error) {
|
||||
message.error("文件删除失败");
|
||||
}
|
||||
}
|
||||
function getStatusLabel(status: string) {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
case "ready":
|
||||
return "已完成";
|
||||
case "processing":
|
||||
return "处理中";
|
||||
case "vectorizing":
|
||||
return "向量化中";
|
||||
case "importing":
|
||||
return "导入中";
|
||||
case "error":
|
||||
return "错误";
|
||||
case "disabled":
|
||||
return "已禁用";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
}
|
||||
function getStatusIcon(status: string) {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
case "ready":
|
||||
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
||||
case "processing":
|
||||
case "vectorizing":
|
||||
return <RefreshCw className="w-4 h-4 text-blue-500 animate-spin" />;
|
||||
case "importing":
|
||||
return <Upload className="w-4 h-4 text-orange-500" />;
|
||||
case "error":
|
||||
return <Trash2 className="w-4 h-4 text-red-500" />;
|
||||
default:
|
||||
return <File className="w-4 h-4 text-gray-400" />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库已删除");
|
||||
navigate("/data/knowledge-base");
|
||||
};
|
||||
|
||||
const handleRefreshPage = () => {
|
||||
fetchKnowledgeBaseDetails(knowledgeBase.id);
|
||||
fetchFiles();
|
||||
setShowEdit(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: () => 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",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
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>
|
||||
),
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
fixed: "left" as const,
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
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",
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "vectorizationStatus",
|
||||
filters: allVectorizationStatuses
|
||||
.filter((opt) => opt.value !== null)
|
||||
.map((opt) => ({
|
||||
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>
|
||||
width: 120,
|
||||
render: (status: any) => (
|
||||
<Badge color={status?.color} text={status?.label} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "分块数",
|
||||
dataIndex: "chunkCount",
|
||||
key: "chunkCount",
|
||||
render: (chunkCount: number) => (
|
||||
<span className="font-medium text-gray-900">{chunkCount}</span>
|
||||
),
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
dataIndex: "uploadedAt",
|
||||
key: "uploadedAt",
|
||||
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: (_: any, file: KBFile) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: "重试",
|
||||
key: "retry",
|
||||
onClick: () => handleStartVectorization(file.id),
|
||||
},
|
||||
{
|
||||
label: "删除",
|
||||
key: "delete",
|
||||
onClick: () => handleDeleteFile(file),
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal />
|
||||
</Dropdown>
|
||||
<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="">
|
||||
{/* Breadcrumb */}
|
||||
<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.Item>{knowledgeBase?.name}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Knowledge Base Header */}
|
||||
<DetailHeader
|
||||
data={{
|
||||
icon:
|
||||
knowledgeBase.type === "structured" ? (
|
||||
<Database className="w-8 h-8" />
|
||||
) : (
|
||||
<BookOpen className="w-8 h-8" />
|
||||
),
|
||||
status: {
|
||||
label: getStatusLabel(knowledgeBase.status),
|
||||
icon: getStatusIcon(knowledgeBase.status),
|
||||
color: getStatusBadgeVariant(knowledgeBase.status),
|
||||
},
|
||||
name: knowledgeBase.name,
|
||||
description: knowledgeBase.description,
|
||||
createdAt: knowledgeBase.createdAt,
|
||||
lastUpdated: knowledgeBase.lastUpdated,
|
||||
}}
|
||||
statistics={[
|
||||
{
|
||||
icon: <File className="w-4 h-4 text-gray-400" />,
|
||||
label: "文件",
|
||||
value: knowledgeBase.fileCount,
|
||||
},
|
||||
{
|
||||
icon: <Layers className="w-4 h-4 text-gray-400" />,
|
||||
label: "分块",
|
||||
value: knowledgeBase.chunkCount?.toLocaleString?.() ?? 0,
|
||||
},
|
||||
{
|
||||
icon: <StarOff className="w-4 h-4 text-gray-400" />,
|
||||
label: "向量",
|
||||
value: knowledgeBase.vectorCount?.toLocaleString?.() ?? 0,
|
||||
},
|
||||
{
|
||||
icon: <Database className="w-4 h-4 text-gray-400" />,
|
||||
label: "大小",
|
||||
value: knowledgeBase.size,
|
||||
},
|
||||
]}
|
||||
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),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
<DetailHeader
|
||||
data={knowledgeBase}
|
||||
statistics={knowledgeBase?.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-1">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(keyword) =>
|
||||
setSearchParams({ ...searchParams, keyword })
|
||||
}
|
||||
searchPlaceholder="搜索文件名..."
|
||||
filters={[]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() =>
|
||||
setSearchParams({ ...searchParams, filter: {} })
|
||||
}
|
||||
showViewToggle={false}
|
||||
showReload={false}
|
||||
/>
|
||||
</div>
|
||||
<AddDataDialog knowledgeBase={knowledgeBase} />
|
||||
</div>
|
||||
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={fileColumns}
|
||||
dataSource={files}
|
||||
rowKey="id"
|
||||
pagination={pagination}
|
||||
scroll={{ y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
{/* 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>
|
||||
{/* 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>
|
||||
);
|
||||
};
|
||||
|
||||
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() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const { message } = App.useApp();
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
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>) => {
|
||||
if (e.target.files) {
|
||||
setSelectedFiles(Array.from(e.target.files));
|
||||
}
|
||||
const dataSourceOptions = [
|
||||
{ label: "本地上传", value: "local" },
|
||||
{ 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 () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
message.error("请先选择文件");
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (isOpen) fetchDatasets();
|
||||
}, [isOpen]);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
selectedFiles.forEach((file) => {
|
||||
formData.append("files", file);
|
||||
const updateTreeData = (list, key: React.Key, children) =>
|
||||
list.map((node) => {
|
||||
if (node.key === key) {
|
||||
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);
|
||||
message.success("文件上传成功");
|
||||
setIsOpen(false);
|
||||
setSelectedFiles([]);
|
||||
} catch (error) {
|
||||
message.error("文件上传失败");
|
||||
}
|
||||
const handleBeforeUpload = (_, files: UploadFile[]) => {
|
||||
setFileList([...fileList, ...files]);
|
||||
return false;
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Button type="primary" onClick={() => setIsOpen(true)}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
添加数据
|
||||
</Button>
|
||||
<Modal
|
||||
title="添加数据文件"
|
||||
title="添加数据"
|
||||
open={isOpen}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
onOk={handleUpload}
|
||||
okText="上传"
|
||||
onOk={handleAddData}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
width={1000}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
accept=".txt,.pdf,.docx,.csv,.json"
|
||||
/>
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4>已选择的文件:</h4>
|
||||
<ul>
|
||||
{selectedFiles.map((file, index) => (
|
||||
<li key={index}>
|
||||
{file.name} - {(file.size / 1024).toFixed(2)} KB
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-auto p-6">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={newKB}
|
||||
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
||||
>
|
||||
<Form.Item
|
||||
label="分块方式"
|
||||
name="processType"
|
||||
required
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select options={sliceOptions}></Select>
|
||||
</Form.Item>
|
||||
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,11 +12,15 @@ import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
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();
|
||||
@@ -74,24 +78,32 @@ export default function CreateKnowledgeBase({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpen(false);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
创建知识库
|
||||
</Button>
|
||||
{showBtn && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
创建知识库
|
||||
</Button>
|
||||
)}
|
||||
<Modal
|
||||
title={isEdit ? "编辑知识库" : "创建知识库"}
|
||||
open={open}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onCancel={() => setOpen(false)}
|
||||
maskClosable={false}
|
||||
onCancel={handleCloseModal}
|
||||
onOk={handleCreateKnowledgeBase}
|
||||
>
|
||||
<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";
|
||||
|
||||
|
||||
// 获取知识库列表
|
||||
export function queryKnowledgeBasesUsingPost(params: any) {
|
||||
console.log('get tk', params);
|
||||
|
||||
console.log("get tk", params);
|
||||
|
||||
return post("/api/knowledge-base/list", params);
|
||||
}
|
||||
|
||||
@@ -28,22 +27,28 @@ export function deleteKnowledgeBaseByIdUsingDelete(baseId: string) {
|
||||
return del(`/api/knowledge-base/${baseId}`);
|
||||
}
|
||||
|
||||
// 获取知识生成任务列表
|
||||
export function queryKnowledgeGenerationTasksUsingPost(params: any) {
|
||||
return post("/api/knowledge-base/tasks", params);
|
||||
// 获取知识生成文件列表
|
||||
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data) {
|
||||
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);
|
||||
}
|
||||
|
||||
// 获取知识生成文件详情
|
||||
export function queryKnowledgeGenerationFilesByIdUsingGet(baseId: string, fileId: string) {
|
||||
export function queryKnowledgeBaseFilesByIdUsingGet(
|
||||
baseId: string,
|
||||
fileId: string
|
||||
) {
|
||||
return get(`/api/knowledge-base/${baseId}/files/${fileId}`);
|
||||
}
|
||||
|
||||
// 删除知识生成文件
|
||||
export function deleteKnowledgeGenerationTaskByIdUsingDelete(baseId: string) {
|
||||
return del(`/api/knowledge-base/${baseId}/files`);
|
||||
export function deleteKnowledgeBaseFileByIdUsingDelete(
|
||||
baseId: string,
|
||||
fileId: string
|
||||
) {
|
||||
return del(`/api/knowledge-base/${baseId}/files/${fileId}`);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,48 @@
|
||||
import {
|
||||
BookOpen,
|
||||
BookOpenText,
|
||||
BookType,
|
||||
ChartNoAxesColumn,
|
||||
CheckCircle,
|
||||
CircleEllipsis,
|
||||
Clock,
|
||||
Database,
|
||||
File,
|
||||
VectorSquare,
|
||||
XCircle,
|
||||
} 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";
|
||||
|
||||
export const KBStatusMap = {
|
||||
[KBStatus.READY]: {
|
||||
label: KBStatus.READY,
|
||||
export const KBFileStatusMap = {
|
||||
[KBFileStatus.PROCESSED]: {
|
||||
value: KBFileStatus.PROCESSED,
|
||||
label: "已处理",
|
||||
icon: CheckCircle,
|
||||
color: "#389e0d",
|
||||
},
|
||||
[KBStatus.VECTORIZING]: {
|
||||
label: KBStatus.PROCESSING,
|
||||
[KBFileStatus.PROCESSING]: {
|
||||
value: KBFileStatus.PROCESSING,
|
||||
label: "处理中",
|
||||
icon: Clock,
|
||||
color: "#3b82f6",
|
||||
color: "#faad14",
|
||||
},
|
||||
[KBStatus.ERROR]: {
|
||||
label: KBStatus.ERROR,
|
||||
[KBFileStatus.PROCESS_FAILED]: {
|
||||
value: KBFileStatus.PROCESS_FAILED,
|
||||
label: "处理失败",
|
||||
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" />,
|
||||
description: kb.description,
|
||||
statistics: [
|
||||
{ label: "索引模型", value: kb.embeddingModel },
|
||||
{ label: "文本理解模型", value: kb.chatModel },
|
||||
{ label: "文件数", value: formatNumber(kb?.fileCount) || 0 },
|
||||
{ label: "大小", value: formatBytes(kb?.size) || "0 MB" },
|
||||
{
|
||||
label: "索引模型",
|
||||
key: "embeddingModel",
|
||||
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),
|
||||
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 {
|
||||
READY = "ready",
|
||||
PROCESSING = "processing",
|
||||
VECTORIZING = "vectorizing",
|
||||
IMPORTING = "importing",
|
||||
ERROR = "error",
|
||||
DISABLED = "disabled",
|
||||
export enum KBFileStatus {
|
||||
UNPROCESSED = "UNPROCESSED",
|
||||
PROCESSING = "PROCESSING",
|
||||
PROCESSED = "PROCESSED",
|
||||
PROCESS_FAILED = "PROCESS_FAILED",
|
||||
}
|
||||
|
||||
export enum KBType {
|
||||
@@ -25,17 +23,17 @@ export interface KnowledgeBaseItem {
|
||||
|
||||
export interface KBFile {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
size: string;
|
||||
status: "processing" | "completed" | "error" | "disabled" | "vectorizing";
|
||||
fileName: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: KBFileStatus;
|
||||
chunkCount: number;
|
||||
progress: number;
|
||||
uploadedAt: string;
|
||||
source: "upload" | "dataset";
|
||||
datasetId?: string;
|
||||
chunks?: Chunk[];
|
||||
vectorizationStatus?: "pending" | "processing" | "completed" | "failed";
|
||||
metadata: Record<string, any>;
|
||||
knowledgeBaseId: string;
|
||||
fileId: string;
|
||||
updatedBy: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
interface Chunk {
|
||||
@@ -74,12 +72,3 @@ interface VectorizationRecord {
|
||||
};
|
||||
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>
|
||||
</div>
|
||||
|
||||
<Progress size="small" percent={Number(task.percent.toFixed(2))} />
|
||||
<Progress size="small" percent={task.percent} />
|
||||
</div>
|
||||
))}
|
||||
{taskList.length === 0 && (
|
||||
|
||||
@@ -31,7 +31,7 @@ import EvaluationTaskCreate from "@/pages/DataEvaluation/Create/CreateTask";
|
||||
import EvaluationTaskReport from "@/pages/DataEvaluation/Report/EvaluationReport";
|
||||
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 KnowledgeBaseFileDetailPage from "@/pages/KnowledgeBase/FileDetail/KnowledgeBaseFileDetail";
|
||||
|
||||
@@ -222,7 +222,7 @@ const router = createBrowserRouter([
|
||||
{
|
||||
path: "",
|
||||
index: true,
|
||||
Component: KnowledgeGenerationPage,
|
||||
Component: KnowledgeBasePage,
|
||||
},
|
||||
{
|
||||
path: "detail/:id",
|
||||
|
||||
Reference in New Issue
Block a user