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:
chenghh-9609
2025-10-31 10:03:42 +08:00
committed by GitHub
parent d89811f238
commit c6958d1511
16 changed files with 974 additions and 769 deletions

View File

@@ -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 }}
> >

View File

@@ -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
) { ) {

View File

@@ -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", // 获取算子列表

View File

@@ -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
); );

View File

@@ -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+)")),
(req, res) => {
const id = req.params.baseId; const id = req.params.baseId;
const item = const item =
knowledgeBaseList.find((kb) => kb.id === id) || KnowledgeBaseItem(); knowledgeBaseList.find((kb) => kb.id === id) || KnowledgeBaseItem();
res.send(item); res.send({
} code: "0",
); msg: "Success",
data: item,
});
});
// 更新知识库 // 更新知识库
router.put(API.updateKnowledgeBaseByIdUsingPut, (req, res) => { router.put(API.updateKnowledgeBaseByIdUsingPut, (req, res) => {
@@ -90,32 +114,8 @@ 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,
});
res.send(tasks);
});
// 添加文件到知识库 // 添加文件到知识库
router.post( router.post(API.addKnowledgeBaseFilesUsingPost, (req, res) => {
new RegExp(
API.addKnowledgeGenerationFilesUsingPost.replace(":baseId", "(\\w+)")
),
(req, res) => {
const file = Mock.mock({ const file = Mock.mock({
id: "@guid", id: "@guid",
name: "@ctitle(5,15)", name: "@ctitle(5,15)",
@@ -124,38 +124,53 @@ module.exports = function (router) {
createdAt: "@datetime", createdAt: "@datetime",
}); });
res.status(201).send(file); 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" });
});
}; };

View File

@@ -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) || "--",

View File

@@ -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);
message.success("文件已删除");
fetchFiles();
} catch (error) {
message.error("文件删除失败");
}
};
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
message.success("知识库已删除");
navigate("/data/knowledge-base");
};
const handleDeleteKB = (kb: KnowledgeBase) => {}; const handleRefreshPage = () => {
fetchKnowledgeBaseDetails(knowledgeBase.id);
fetchFiles();
setShowEdit(false);
};
// 状态 Badge 映射 const operations = [
function getStatusBadgeVariant(status: string) { {
switch (status) { key: "edit",
case "completed": label: "编辑知识库",
case "ready": icon: <EditOutlined className="w-4 h-4" />,
return "success"; onClick: () => {
case "processing": setShowEdit(true);
case "vectorizing": },
return "processing"; },
case "importing": {
return "warning"; key: "refresh",
case "error": label: "刷新知识库",
return "error"; icon: <ReloadOutlined className="w-4 h-4" />,
default: onClick: () => {
return "default"; handleRefreshPage();
} },
} },
function getStatusLabel(status: string) { {
switch (status) { key: "delete",
case "completed": label: "删除知识库",
case "ready": danger: true,
return "已完成"; confirm: {
case "processing": title: "确认删除该知识库吗?",
return "处理中"; description: "删除后将无法恢复,请谨慎操作。",
case "vectorizing": cancelText: "取消",
return "向量化中"; okText: "删除",
case "importing": okType: "danger",
return "导入中"; onConfirm: () => handleDeleteKB(knowledgeBase),
case "error": },
return "错误"; icon: <DeleteOutlined className="w-4 h-4" />,
case "disabled": },
return "已禁用"; ];
default:
return "未知"; const fileOps = [
} {
} key: "delete",
function getStatusIcon(status: string) { label: "删除文件",
switch (status) { icon: <DeleteOutlined className="w-4 h-4" />,
case "completed": danger: true,
case "ready": onClick: handleDeleteFile,
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 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">
{/* Knowledge Base Header */}
<DetailHeader <DetailHeader
data={{ data={knowledgeBase}
icon: statistics={knowledgeBase?.statistics || []}
knowledgeBase.type === "structured" ? ( operations={operations}
<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),
},
],
},
]}
/> />
{/* Tab Navigation */} <CreateKnowledgeBase
<Card> showBtn={false}
{/* Files Section */} isEdit={showEdit}
<div className="flex items-center justify-between mb-4 gap-4"> 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"> <div className="flex-1">
<SearchControls <SearchControls
searchTerm={fileSearchQuery} searchTerm={searchParams.keyword}
onSearchChange={setFileSearchQuery} onSearchChange={(keyword) =>
setSearchParams({ ...searchParams, keyword })
}
searchPlaceholder="搜索文件名..." searchPlaceholder="搜索文件名..."
filters={[ filters={[]}
{ onFiltersChange={handleFiltersChange}
key: "status", onClearFilters={() =>
label: "状态筛选", setSearchParams({ ...searchParams, filter: {} })
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} showViewToggle={false}
showReload={false}
/> />
</div> </div>
<Button type="primary"> <AddDataDialog knowledgeBase={knowledgeBase} />
<Plus className="w-4 h-4 mr-1" />
</Button>
</div> </div>
{/* Files Table */}
<Table <Table
loading={loading}
columns={fileColumns} columns={fileColumns}
dataSource={files} dataSource={files}
rowKey="id" rowKey="id"
pagination={false} pagination={pagination}
locale={{ scroll={{ y: "calc(100vh - 30rem)" }}
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> </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>
); );
}; };

View 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>
);
}

View File

@@ -1,65 +1,320 @@
export default function AddDataDialog() { 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 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 [isOpen, setIsOpen] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const { message } = App.useApp(); const { message } = App.useApp();
const [form] = Form.useForm();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { // Form initial values
if (e.target.files) { const [newKB, setNewKB] = useState<Partial<KnowledgeBaseItem>>({
setSelectedFiles(Array.from(e.target.files)); dataSource: "dataset",
} processType: "DEFAULT_CHUNK",
}; chunkSize: 500,
overlap: 50,
const handleUpload = async () => { datasetIds: [],
if (selectedFiles.length === 0) {
message.error("请先选择文件");
return;
}
try {
const formData = new FormData();
selectedFiles.forEach((file) => {
formData.append("files", file);
}); });
await uploadDataFilesUsingPost(formData); const [filesTree, setFilesTree] = useState<any[]>([]);
message.success("文件上传成功");
setIsOpen(false); const fetchDatasets = async () => {
setSelectedFiles([]); const { data } = await queryDatasetsUsingGet({
} catch (error) { page: 0,
message.error("文件上传失败"); 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);
};
useEffect(() => {
if (isOpen) fetchDatasets();
}, [isOpen]);
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();
});
});
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 ( 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 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> </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>
</> </>
); );

View File

@@ -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,8 +78,14 @@ export default function CreateKnowledgeBase({
} }
}; };
const handleCloseModal = () => {
setOpen(false);
onClose?.();
};
return ( return (
<> <>
{showBtn && (
<Button <Button
type="primary" type="primary"
icon={<PlusOutlined />} icon={<PlusOutlined />}
@@ -86,12 +96,14 @@ export default function CreateKnowledgeBase({
> >
</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">

View File

@@ -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;

View File

@@ -1,9 +1,8 @@
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}`);
} }

View File

@@ -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",
},
};
}

View File

@@ -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>;
}

View File

@@ -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 && (

View File

@@ -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",