From c6958d1511783c74e24ad41494377c919d3cb087 Mon Sep 17 00:00:00 2001 From: chenghh-9609 <55340429+chenghh-9609@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:03:42 +0800 Subject: [PATCH] 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. --- frontend/src/components/DetailHeader.tsx | 13 +- frontend/src/hooks/useFetchData.ts | 2 +- frontend/src/mock/mock-apis.cjs | 9 +- .../src/mock/mock-seed/data-management.cjs | 2 - .../src/mock/mock-seed/knowledge-base.cjs | 153 ++-- .../pages/DataManagement/dataset.const.tsx | 1 + .../Detail/KnowledgeBaseDetail.tsx | 758 +++++------------- .../KnowledgeBase/Home/KnowledgeBasePage.tsx | 195 +++++ .../components/AddDataDialog.tsx | 347 ++++++-- .../components/CreateKnowledgeBase.tsx | 34 +- .../components/TableTransfer.tsx | 75 ++ .../pages/KnowledgeBase/knowledge-base.api.ts | 25 +- .../KnowledgeBase/knowledge-base.const.tsx | 82 +- .../KnowledgeBase/knowledge-base.model.ts | 41 +- frontend/src/pages/Layout/TaskUpload.tsx | 2 +- frontend/src/routes/routes.ts | 4 +- 16 files changed, 974 insertions(+), 769 deletions(-) create mode 100644 frontend/src/pages/KnowledgeBase/Home/KnowledgeBasePage.tsx create mode 100644 frontend/src/pages/KnowledgeBase/components/TableTransfer.tsx diff --git a/frontend/src/components/DetailHeader.tsx b/frontend/src/components/DetailHeader.tsx index 2b77f45..3f4a942 100644 --- a/frontend/src/components/DetailHeader.tsx +++ b/frontend/src/components/DetailHeader.tsx @@ -39,7 +39,7 @@ interface DetailHeaderProps { } function DetailHeader({ - data, + data = {} as T, statistics, operations, tagConfig, @@ -59,7 +59,7 @@ function DetailHeader({
-

{data.name}

+

{data?.name}

{data?.status && (
@@ -86,7 +86,7 @@ function DetailHeader({ )}
)} -

{data.description}

+

{data?.description}

{statistics.map((stat) => (
@@ -112,13 +112,10 @@ function DetailHeader({ { - op?.onClick(); + op?.confirm?.onConfirm?.(); }} - okText={op.confirm.okText || "确定"} - cancelText={op.confirm.cancelText || "取消"} okType={op.danger ? "danger" : "primary"} overlayStyle={{ zIndex: 9999 }} > diff --git a/frontend/src/hooks/useFetchData.ts b/frontend/src/hooks/useFetchData.ts index 6146c9d..eb27d05 100644 --- a/frontend/src/hooks/useFetchData.ts +++ b/frontend/src/hooks/useFetchData.ts @@ -21,7 +21,7 @@ export default function useFetchData( fetchFunc: (params?: any) => Promise, mapDataFunc: (data: Partial) => T = (data) => data as T, pollingInterval: number = 30000, // 默认30秒轮询一次 - autoRefresh: boolean = true, + autoRefresh: boolean = false, // 是否自动开始轮询,默认 false additionalPollingFuncs: (() => Promise)[] = [], // 额外的轮询函数 pageOffset: number = 1 ) { diff --git a/frontend/src/mock/mock-apis.cjs b/frontend/src/mock/mock-apis.cjs index 0752764..138b072 100644 --- a/frontend/src/mock/mock-apis.cjs +++ b/frontend/src/mock/mock-apis.cjs @@ -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", // 获取算子列表 diff --git a/frontend/src/mock/mock-seed/data-management.cjs b/frontend/src/mock/mock-seed/data-management.cjs index 4a66765..aa9f953 100644 --- a/frontend/src/mock/mock-seed/data-management.cjs +++ b/frontend/src/mock/mock-seed/data-management.cjs @@ -161,8 +161,6 @@ module.exports = function (router) { ); } if (type) { - console.log("filter type:", type); - filteredDatasets = filteredDatasets.filter( (dataset) => dataset.datasetType === type ); diff --git a/frontend/src/mock/mock-seed/knowledge-base.cjs b/frontend/src/mock/mock-seed/knowledge-base.cjs index 923fcf0..28f9164 100644 --- a/frontend/src/mock/mock-seed/knowledge-base.cjs +++ b/frontend/src/mock/mock-seed/knowledge-base.cjs @@ -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" }); + }); }; diff --git a/frontend/src/pages/DataManagement/dataset.const.tsx b/frontend/src/pages/DataManagement/dataset.const.tsx index 8950506..4bda54a 100644 --- a/frontend/src/pages/DataManagement/dataset.const.tsx +++ b/frontend/src/pages/DataManagement/dataset.const.tsx @@ -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) || "--", diff --git a/frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx b/frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx index 9909325..a194903 100644 --- a/frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx +++ b/frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx @@ -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(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( + (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 ; - case "processing": - case "vectorizing": - return ; - case "importing": - return ; - case "error": - return ; - default: - return ; - } - } + }; + + 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: , + onClick: () => { + setShowEdit(true); + }, + }, + { + key: "refresh", + label: "刷新知识库", + icon: , + onClick: () => { + handleRefreshPage(); + }, + }, + { + key: "delete", + label: "删除知识库", + danger: true, + confirm: { + title: "确认删除该知识库吗?", + description: "删除后将无法恢复,请谨慎操作。", + cancelText: "取消", + okText: "删除", + okType: "danger", + onConfirm: () => handleDeleteKB(knowledgeBase), + }, + icon: , + }, + ]; + + const fileOps = [ + { + key: "delete", + label: "删除文件", + icon: , + danger: true, + onClick: handleDeleteFile, + }, + ]; const fileColumns = [ { title: "文件名", dataIndex: "name", key: "name", - filterDropdown: ({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters, - }: any) => ( -
- - setSelectedKeys(e.target.value ? [e.target.value] : []) - } - onPressEnter={confirm} - style={{ width: 188, marginBottom: 8, display: "block" }} - /> - - -
- ), - onFilter: (value: string, record: KBFile) => - record.name.toLowerCase().includes(value.toLowerCase()), - render: (text: string, file: KBFile) => ( - - ), + 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) => ( -
- - {file.vectorizationStatus === "processing" && ( -
- -
- )} -
- ), - }, - { - title: "来源", - dataIndex: "source", - key: "source", - render: (_: any, file: KBFile) => ( -
- - {file.datasetId && ( - ({file.datasetId}) - )} -
+ width: 120, + render: (status: any) => ( + ), }, { title: "分块数", dataIndex: "chunkCount", key: "chunkCount", - render: (chunkCount: number) => ( - {chunkCount} - ), + 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) => ( - handleStartVectorization(file.id), - }, - { - label: "删除", - key: "delete", - onClick: () => handleDeleteFile(file), - }, - ], - }} - > - - +
+ {fileOps.map((op) => ( + +
), }, ]; return ( -
- {/* Breadcrumb */} +
navigate("/data/knowledge-base")}>知识库 - {knowledgeBase.name} + {knowledgeBase?.name}
-
- {/* Knowledge Base Header */} - - ) : ( - - ), - 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: , - label: "文件", - value: knowledgeBase.fileCount, - }, - { - icon: , - label: "分块", - value: knowledgeBase.chunkCount?.toLocaleString?.() ?? 0, - }, - { - icon: , - label: "向量", - value: knowledgeBase.vectorCount?.toLocaleString?.() ?? 0, - }, - { - icon: , - label: "大小", - value: knowledgeBase.size, - }, - ]} - operations={[ - { - key: "edit", - label: "修改参数配置", - icon: , - onClick: () => { - setEditForm(knowledgeBase); - setCurrentView("config"); - }, - }, - { - key: "vector", - label: "向量化管理", - icon: , - onClick: () => setShowVectorizationDialog(true), - }, - ...(knowledgeBase.status === "error" - ? [ - { - key: "retry", - label: "重试", - onClick: () => {}, // 填写重试逻辑 - danger: false, - }, - ] - : []), - { - key: "more", - label: "更多操作", - icon: , - isDropdown: true, - items: [ - { - key: "download", - label: "导出", - }, - { - key: "settings", - label: "配置", - }, - { type: "divider" }, - { - key: "delete", - label: "删除", - danger: true, - onClick: () => handleDeleteKB(knowledgeBase), - }, - ], - }, - ]} + + setShowEdit(false)} + /> +
+
+
+ + setSearchParams({ ...searchParams, keyword }) + } + searchPlaceholder="搜索文件名..." + filters={[]} + onFiltersChange={handleFiltersChange} + onClearFilters={() => + setSearchParams({ ...searchParams, filter: {} }) + } + showViewToggle={false} + showReload={false} + /> +
+ +
+ + - {/* Tab Navigation */} - - {/* Files Section */} -
-
- { - setFileStatusFilter(filters.status?.[0] || "all"); - }} - showViewToggle={false} - /> -
- -
- - {/* Files Table */} -
- -

- 没有找到文件 -

-

- 尝试调整搜索条件或添加新文件 -

- - - ), - }} - /> - - {/* Vectorization Dialog */} - setShowVectorizationDialog(false)} - footer={null} - title="向量化管理" - width={700} - destroyOnClose - > -
-
- -

当前状态

-
-
- 已向量化文件: - - { - knowledgeBase.files.filter( - (f) => f.vectorizationStatus === "completed" - ).length - } - /{knowledgeBase.files.length} - -
-
- 向量总数: - - {knowledgeBase.vectorCount?.toLocaleString?.() ?? 0} - -
-
- 存储大小: - {knowledgeBase.size} -
-
-
- -

操作选项

-
- - - -
-
-
- -
-

文件向量化状态

-
- {knowledgeBase.files.map((file: KBFile) => ( -
-
- -
-

{file.name}

-

- {file.chunkCount} 个分块 -

-
-
-
- - {file.vectorizationStatus === "processing" && ( -
- -
- )} - {file.vectorizationStatus !== "completed" && ( - - )} -
-
- ))} -
-
-
- -
-
-
- {/* Edit File Dialog */} - setShowEditFileDialog(null)} - title="编辑文件" - width={600} - footer={[ - , - , - ]} - destroyOnClose - > -
-
-
- - -
-
- - -
-
- - {showEditFileDialog?.source === "upload" ? ( -
- -
- -

- 拖拽或点击上传新版本文件 -

- -
-
- ) : ( -
- -
-
- - 当前数据集: {showEditFileDialog?.datasetId} - - -
-

- 此文件来自数据集,可以选择更新数据集中的对应文件或切换到其他数据集文件 -

-
-
- )} - -
- -
-
- - 更新后重新处理分块 -
-
- - 重新生成向量 -
-
-
-
-
); }; diff --git a/frontend/src/pages/KnowledgeBase/Home/KnowledgeBasePage.tsx b/frontend/src/pages/KnowledgeBase/Home/KnowledgeBasePage.tsx new file mode 100644 index 0000000..c372fd7 --- /dev/null +++ b/frontend/src/pages/KnowledgeBase/Home/KnowledgeBasePage.tsx @@ -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(null); + const { + loading, + tableData, + searchParams, + pagination, + fetchData, + setSearchParams, + handleFiltersChange, + } = useFetchData( + 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: , + onClick: (item) => { + setIsEdit(true); + setCurrentKB(item); + }, + }, + { + key: "delete", + label: "删除", + danger: true, + icon: , + 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) => ( + + ), + }, + { + 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) => ( +
+ {operations.map((op) => ( + +
+ ), + }, + ]; + // Main list view + return ( +
+
+

知识生成

+ { + fetchData(); + }} + onClose={() => { + setIsEdit(false); + setCurrentKB(null); + }} + /> +
+ + + setSearchParams({ ...searchParams, keyword }) + } + searchPlaceholder="搜索知识库..." + filters={[]} + onFiltersChange={handleFiltersChange} + onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })} + viewMode={viewMode} + onViewModeChange={setViewMode} + showViewToggle + onReload={fetchData} + /> + {viewMode === "card" ? ( + navigate(`/data/knowledge-base/detail/${item.id}`)} + pagination={pagination} + /> + ) : ( + +
+ + )} + + ); +} diff --git a/frontend/src/pages/KnowledgeBase/components/AddDataDialog.tsx b/frontend/src/pages/KnowledgeBase/components/AddDataDialog.tsx index 1596946..13d6d1d 100644 --- a/frontend/src/pages/KnowledgeBase/components/AddDataDialog.tsx +++ b/frontend/src/pages/KnowledgeBase/components/AddDataDialog.tsx @@ -1,65 +1,320 @@ -export default function AddDataDialog() { - const [isOpen, setIsOpen] = useState(false); - const [selectedFiles, setSelectedFiles] = useState([]); - 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) => { - 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([]); + + // Form initial values + const [newKB, setNewKB] = useState>({ + dataSource: "dataset", + processType: "DEFAULT_CHUNK", + chunkSize: 500, + overlap: 50, + datasetIds: [], + }); + + const [filesTree, setFilesTree] = useState([]); + + 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((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 ( <> - setIsOpen(false)} - onOk={handleUpload} - okText="上传" + onOk={handleAddData} + okText="确定" + cancelText="取消" + width={1000} > - - {selectedFiles.length > 0 && ( -
-

已选择的文件:

-
    - {selectedFiles.map((file, index) => ( -
  • - {file.name} - {(file.size / 1024).toFixed(2)} KB -
  • - ))} -
-
- )} +
+
setNewKB(allValues)} + > + + + + +
+ + + + + + +
+ {newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && ( + + + + )} + + + + {newKB.dataSource === "local" && ( + + +

+ +

+

本地文件上传

+

+ 拖拽文件到此处或点击选择文件 +

+
+
+ )} + {newKB.dataSource === "dataset" && ( + +
+ { + 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, + })), + }); + }} + /> +
+
+ )} + +
); diff --git a/frontend/src/pages/KnowledgeBase/components/CreateKnowledgeBase.tsx b/frontend/src/pages/KnowledgeBase/components/CreateKnowledgeBase.tsx index 0316f08..fb1d940 100644 --- a/frontend/src/pages/KnowledgeBase/components/CreateKnowledgeBase.tsx +++ b/frontend/src/pages/KnowledgeBase/components/CreateKnowledgeBase.tsx @@ -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 | 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 ( <> - + {showBtn && ( + + )} setOpen(false)} + maskClosable={false} + onCancel={handleCloseModal} onOk={handleCreateKnowledgeBase} >
diff --git a/frontend/src/pages/KnowledgeBase/components/TableTransfer.tsx b/frontend/src/pages/KnowledgeBase/components/TableTransfer.tsx new file mode 100644 index 0000000..76a2666 --- /dev/null +++ b/frontend/src/pages/KnowledgeBase/components/TableTransfer.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { Table, Transfer } from "antd"; +import type { + GetProp, + TableColumnsType, + TableProps, + TransferProps, +} from "antd"; + +type TransferItem = GetProp[number]; +type TableRowSelection = TableProps["rowSelection"]; + +interface DataType { + key: string; + title: string; + description: string; +} + +interface TableTransferProps extends TransferProps { + dataSource: DataType[]; + leftColumns: TableColumnsType; + rightColumns: TableColumnsType; +} + +// Customize Table Transfer +const TableTransfer: React.FC = (props) => { + const { leftColumns, rightColumns, ...restProps } = props; + return ( + + {({ + direction, + filteredItems, + onItemSelect, + onItemSelectAll, + selectedKeys: listSelectedKeys, + disabled: listDisabled, + }) => { + const columns = direction === "left" ? leftColumns : rightColumns; + const rowSelection: TableRowSelection = { + getCheckboxProps: () => ({ disabled: listDisabled }), + onChange(selectedRowKeys) { + onItemSelectAll(selectedRowKeys, "replace"); + }, + selectedRowKeys: listSelectedKeys, + selections: [ + Table.SELECTION_ALL, + Table.SELECTION_INVERT, + Table.SELECTION_NONE, + ], + }; + + return ( +
({ + onClick: () => { + if (itemDisabled || listDisabled) { + return; + } + onItemSelect(key, !listSelectedKeys.includes(key)); + }, + })} + /> + ); + }} + + ); +}; + +export default TableTransfer; diff --git a/frontend/src/pages/KnowledgeBase/knowledge-base.api.ts b/frontend/src/pages/KnowledgeBase/knowledge-base.api.ts index 60e8d44..c69a37a 100644 --- a/frontend/src/pages/KnowledgeBase/knowledge-base.api.ts +++ b/frontend/src/pages/KnowledgeBase/knowledge-base.api.ts @@ -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}`); } diff --git a/frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx b/frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx index 7980619..fdbece0 100644 --- a/frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx +++ b/frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx @@ -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: , 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: , + value: kb.embeddingModel, + }, + { + label: "文本理解模型", + key: "chatModel", + icon: , + value: kb.chatModel, + }, + { + label: "文件数", + key: "fileCount", + icon: , + value: formatNumber(kb?.fileCount) || 0, + }, + { + label: "大小", + key: "size", + icon: , + value: formatBytes(kb?.size) || "0 MB", + }, ], updatedAt: formatDateTime(kb.updatedAt), createdAt: formatDateTime(kb.createdAt), }; } + +export function mapFileData(file: Partial): 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", + }, + }; +} diff --git a/frontend/src/pages/KnowledgeBase/knowledge-base.model.ts b/frontend/src/pages/KnowledgeBase/knowledge-base.model.ts index 1a030b5..8c9bf29 100644 --- a/frontend/src/pages/KnowledgeBase/knowledge-base.model.ts +++ b/frontend/src/pages/KnowledgeBase/knowledge-base.model.ts @@ -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; + 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; -} diff --git a/frontend/src/pages/Layout/TaskUpload.tsx b/frontend/src/pages/Layout/TaskUpload.tsx index 3c0e1c7..9f5221f 100644 --- a/frontend/src/pages/Layout/TaskUpload.tsx +++ b/frontend/src/pages/Layout/TaskUpload.tsx @@ -53,7 +53,7 @@ export default function TaskUpload() { > - + ))} {taskList.length === 0 && ( diff --git a/frontend/src/routes/routes.ts b/frontend/src/routes/routes.ts index 25a8749..e415e00 100644 --- a/frontend/src/routes/routes.ts +++ b/frontend/src/routes/routes.ts @@ -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",