From fdfcfec1f193fd13b7dd0a44f5dd4c53916610ac Mon Sep 17 00:00:00 2001 From: chenghh-9609 <55340429+chenghh-9609@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:39:26 +0800 Subject: [PATCH] update knowledge base file selection component (#96) * feat: Implement DatasetFileTransfer component for file selection and management * feat: Add pagination support to file list in Overview component --- .../src/components/DatasetFileTransfer.tsx | 248 +++++++++++++++++ .../Detail/components/Overview.tsx | 6 +- .../Detail/useFilesOperation.ts | 11 +- .../components/AddDataDialog.tsx | 90 ++++--- .../components/DatasetFileTransfer.tsx | 253 ------------------ .../KnowledgeBase/knowledge-base.const.tsx | 65 +++-- .../src/pages/SynthesisTask/CreateTask.tsx | 3 +- 7 files changed, 364 insertions(+), 312 deletions(-) create mode 100644 frontend/src/components/DatasetFileTransfer.tsx delete mode 100644 frontend/src/pages/KnowledgeBase/components/DatasetFileTransfer.tsx diff --git a/frontend/src/components/DatasetFileTransfer.tsx b/frontend/src/components/DatasetFileTransfer.tsx new file mode 100644 index 0000000..a92ed86 --- /dev/null +++ b/frontend/src/components/DatasetFileTransfer.tsx @@ -0,0 +1,248 @@ +import React, { useEffect } from "react"; +import { Button, Input, Table } from "antd"; +import { RightOutlined } from "@ant-design/icons"; +import { mapDataset } from "@/pages/DataManagement/dataset.const"; +import { + Dataset, + DatasetFile, + DatasetType, +} from "@/pages/DataManagement/dataset.model"; +import { + queryDatasetFilesUsingGet, + queryDatasetsUsingGet, +} from "@/pages/DataManagement/dataset.api"; +import { formatBytes } from "@/utils/unit"; +import { useDebouncedEffect } from "@/hooks/useDebouncedEffect"; +import { DatasetFileCols as fileCols } from "../pages/KnowledgeBase/knowledge-base.const"; + +interface DatasetFileTransferProps + extends React.HTMLAttributes { + open: boolean; + selectedFilesMap: { [key: string]: DatasetFile }; + onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void; +} + +// Customize Table Transfer +const DatasetFileTransfer: React.FC = ({ + open, + selectedFilesMap, + onSelectedFilesChange, + ...props +}) => { + const [datasets, setDatasets] = React.useState([]); + const [datasetSearch, setDatasetSearch] = React.useState(""); + const [datasetPagination, setDatasetPagination] = React.useState<{ + current: number; + pageSize: number; + total: number; + }>({ current: 1, pageSize: 10, total: 0 }); + + const [files, setFiles] = React.useState([]); + const [filesSearch, setFilesSearch] = React.useState(""); + const [filesPagination, setFilesPagination] = React.useState<{ + current: number; + pageSize: number; + total: number; + }>({ current: 1, pageSize: 10, total: 0 }); + + const [showFiles, setShowFiles] = React.useState(false); + const [selectedDataset, setSelectedDataset] = React.useState( + null + ); + const [datasetSelections, setDatasetSelections] = React.useState( + [] + ); + + const fetchDatasets = async () => { + const { data } = await queryDatasetsUsingGet({ + page: datasetPagination.current - 1, + size: datasetPagination.pageSize, + keyword: datasetSearch, + type: DatasetType.TEXT, + }); + setDatasets(data.content.map(mapDataset) || []); + setDatasetPagination((prev) => ({ + ...prev, + total: data.totalElements, + })); + }; + + useDebouncedEffect( + () => { + fetchDatasets(); + }, + [datasetSearch, datasetPagination.pageSize, datasetPagination.current], + 300 + ); + + const fetchFiles = async () => { + if (!selectedDataset) return; + const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, { + page: filesPagination.current - 1, + size: filesPagination.pageSize, + keyword: filesSearch, + }); + setFiles( + data.content.map((item) => ({ + ...item, + key: item.id, + datasetName: selectedDataset.name, + })) || [] + ); + setFilesPagination((prev) => ({ + ...prev, + total: data.totalElements, + })); + }; + + useEffect(() => { + if (selectedDataset) { + fetchFiles(); + } + }, [selectedDataset]); + + const toggleSelectFile = (record: DatasetFile) => { + if (!selectedFilesMap[record.id]) { + onSelectedFilesChange({ + ...selectedFilesMap, + [record.id]: record, + }); + } else { + const newSelectedFiles = { ...selectedFilesMap }; + delete newSelectedFiles[record.id]; + onSelectedFilesChange(newSelectedFiles); + } + }; + + useEffect(() => { + if (!open) { + // 重置状态 + setDatasets([]); + setDatasetSearch(""); + setDatasetPagination({ current: 1, pageSize: 10, total: 0 }); + setFiles([]); + setFilesSearch(""); + setFilesPagination({ current: 1, pageSize: 10, total: 0 }); + setShowFiles(false); + setSelectedDataset(null); + setDatasetSelections([]); + } + }, [open]); + + const datasetCols = [ + { + title: "数据集名称", + dataIndex: "name", + key: "name", + ellipsis: true, + }, + { + title: "文件数", + dataIndex: "fileCount", + key: "fileCount", + ellipsis: true, + }, + { + title: "大小", + dataIndex: "totalSize", + key: "totalSize", + ellipsis: true, + render: formatBytes, + }, + ]; + + return ( +
+
+
+
选择数据集
+
+ setDatasetSearch(e.target.value)} + /> +
+ + selectedDataset?.id === record.id ? "bg-blue-100" : "" + } + onRow={(record: Dataset) => ({ + onClick: () => { + setSelectedDataset(record); + if (!datasetSelections.find((d) => d.id === record.id)) { + setDatasetSelections([...datasetSelections, record]); + } else { + setDatasetSelections( + datasetSelections.filter((d) => d.id !== record.id) + ); + } + }, + })} + dataSource={datasets} + columns={datasetCols} + pagination={datasetPagination} + /> + + +
+
选择文件
+
+ setFilesSearch(e.target.value)} + /> +
+
({ + onClick: () => toggleSelectFile(record), + })} + rowSelection={{ + type: "checkbox", + onSelectAll: (selected, _, changeRows) => { + const newSelectedFiles = { ...selectedFilesMap }; + if (selected) { + changeRows.forEach((row) => { + newSelectedFiles[row.id] = row; + }); + } else { + changeRows.forEach((row) => { + delete newSelectedFiles[row.id]; + }); + } + onSelectedFilesChange(newSelectedFiles); + }, + selectedRowKeys: Object.keys(selectedFilesMap), + onSelect: toggleSelectFile, + }} + /> + + + +
+ + + ); +}; + +export default DatasetFileTransfer; diff --git a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx index 7964da7..08b19af 100644 --- a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx +++ b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx @@ -6,6 +6,7 @@ import { datasetTypeMap } from "../../dataset.const"; export default function Overview({ dataset, filesOperation }) { const { fileList, + pagination, selectedFiles, setSelectedFiles, previewVisible, @@ -179,7 +180,10 @@ export default function Overview({ dataset, filesOperation }) { dataSource={fileList} // rowSelection={rowSelection} scroll={{ x: "max-content", y: 600 }} - pagination={{ showTotal: (total) => `共 ${total} 条` }} + pagination={{ + ...pagination, + showTotal: (total) => `共 ${total} 条`, + }} /> diff --git a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts index cf8f2fe..5d6e2a1 100644 --- a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts +++ b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts @@ -19,6 +19,11 @@ export function useFilesOperation(dataset: Dataset) { // 文件相关状态 const [fileList, setFileList] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]); + const [pagination, setPagination] = useState<{ + current: number; + pageSize: number; + total: number; + }>({ current: 1, pageSize: 10, total: 0 }); // 文件预览相关状态 const [previewVisible, setPreviewVisible] = useState(false); @@ -26,7 +31,10 @@ export function useFilesOperation(dataset: Dataset) { const [previewFileName, setPreviewFileName] = useState(""); const fetchFiles = async () => { - const { data } = await queryDatasetFilesUsingGet(id!); + const { data } = await queryDatasetFilesUsingGet(id!, { + page: pagination.current - 1, + size: pagination.pageSize, + }); setFileList(data.content || []); }; @@ -105,6 +113,7 @@ export function useFilesOperation(dataset: Dataset) { fileList, selectedFiles, setSelectedFiles, + setPagination, previewVisible, setPreviewVisible, previewContent, diff --git a/frontend/src/pages/KnowledgeBase/components/AddDataDialog.tsx b/frontend/src/pages/KnowledgeBase/components/AddDataDialog.tsx index 542a216..4ae9715 100644 --- a/frontend/src/pages/KnowledgeBase/components/AddDataDialog.tsx +++ b/frontend/src/pages/KnowledgeBase/components/AddDataDialog.tsx @@ -8,12 +8,13 @@ import { Modal, Steps, Descriptions, + Table, } from "antd"; import { PlusOutlined } from "@ant-design/icons"; import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api"; -import DatasetFileTransfer from "./DatasetFileTransfer"; +import DatasetFileTransfer from "../../../components/DatasetFileTransfer"; import { DescriptionsItemType } from "antd/es/descriptions"; -import { DatasetFile } from "@/pages/DataManagement/dataset.model"; +import { DatasetFileCols } from "../knowledge-base.const"; const sliceOptions = [ { label: "默认分块", value: "DEFAULT_CHUNK" }, @@ -29,9 +30,7 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { const [form] = Form.useForm(); const [currentStep, setCurrentStep] = useState(0); - const [selectedMap, setSelectedMap] = useState>( - {} - ); + const [selectedFilesMap, setSelectedFilesMap] = useState({}); // 定义分块选项 const sliceOptions = [ @@ -67,8 +66,8 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { // 获取已选择文件总数 const getSelectedFilesCount = () => { - return Object.values(selectedMap).reduce( - (total, files) => total + files.length, + return Object.values(selectedFilesMap).reduce( + (total, ids) => total + ids.length, 0 ); }; @@ -117,23 +116,13 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { delimiter: "", }); form.resetFields(); + setSelectedFilesMap({}); }; const handleAddData = async () => { - const files = []; + const selectedFiles = []; - Object.entries(selectedMap).forEach(([datasetId, fileList]) => { - files.push( - ...fileList.map((file) => ({ - ...file, - id: file.id, - name: file.fileName, - datasetId, - })) - ); - }); - - if (files.length === 0) { + if (selectedFiles.length === 0) { message.warning("请至少选择一个文件"); return; } @@ -141,7 +130,7 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { try { // 构造符合API要求的请求数据 const requestData = { - files, + files: Object.entries(selectedFilesMap), processType: newKB.processType, chunkSize: Number(newKB.chunkSize), // 确保是数字类型 overlapSize: Number(newKB.overlapSize), // 确保是数字类型 @@ -155,7 +144,6 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { message.success("数据添加成功"); // 重置状态 - handleReset(); setOpen(false); } catch (error) { message.error("数据添加失败,请重试"); @@ -164,7 +152,6 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { }; const handleModalCancel = () => { - handleReset(); setOpen(false); }; @@ -179,15 +166,10 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { key: "dataSource", children: "数据集", }, - { - label: "选择的数据集数", - key: "selectedDatasetCount", - children: Object.keys(selectedMap).length, - }, { label: "文件总数", key: "totalFileCount", - children: getSelectedFilesCount(), + children: Object.keys(selectedFilesMap).length, }, { label: "分块方式", @@ -214,6 +196,20 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { }, ] : []), + { + label: "文件列表", + key: "fileList", + span: 3, + children: ( +
+ ), + }, ]; return ( @@ -221,7 +217,10 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { @@ -231,13 +230,25 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { onCancel={handleModalCancel} footer={
+ {currentStep === 0 && ( + + )} {currentStep > 0 && ( )} {currentStep < steps.length - 1 ? ( - ) : ( @@ -259,12 +270,13 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) { /> {/* 步骤内容 */} -
({ - onClick: () => { - const isExpanded = expandedRowKeys.includes(record.id); - onExpand(!isExpanded, record); - }, - })} - dataSource={datasets} - columns={datasetCols} - pagination={datasetPagination} - rowSelection={{ - type: "checkbox", - selectedRowKeys: Object.keys(selectedMap), - onSelect: async (record, isSelected) => { - let files = []; - if (!loadedFiles[record.id]) { - files = await fetchFiles(record); - } else { - files = loadedFiles[record.id]; - } - - const newMap = { ...selectedMap }; - if (isSelected) { - newMap[record.id] = files; - } else { - delete newMap[record.id]; - } - onSelectedChange(newMap); - }, - }} - expandable={{ - expandedRowKeys, - onExpand, - expandedRowRender: (dataset) => ( -
({ - onClick: () => toggleSelectFile(dataset, record), - })} - rowSelection={{ - type: "checkbox", - selectedRowKeys: Object.values( - selectedMap[dataset.id] || {} - ).map((file) => file.id), - onSelect: (record) => toggleSelectFile(dataset, record), - }} - /> - ), - }} - /> - - -
-
- 已选文件({selectedFiles.length}) -
-
- setFilesSearch(e.target.value)} - /> -
-
- - - ); -}; - -export default DatasetFileTransfer; diff --git a/frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx b/frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx index 20c912c..c11b090 100644 --- a/frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx +++ b/frontend/src/pages/KnowledgeBase/knowledge-base.const.tsx @@ -63,26 +63,37 @@ export const KBTypeMap = { }, }; -export function mapKnowledgeBase(kb: KnowledgeBaseItem, showModelFields: boolean = true): KnowledgeBaseItem { +export function mapKnowledgeBase( + kb: KnowledgeBaseItem, + showModelFields: boolean = true +): KnowledgeBaseItem { return { ...kb, icon: , description: kb.description, statistics: [ - ...(showModelFields ? [ - { - label: "索引模型", - key: "embeddingModel", - icon: , - value: kb.embedding?.modelName + (kb.embedding?.provider ? ` (${kb.embedding.provider})` : "") || "无", - }, - { - label: "文本理解模型", - key: "chatModel", - icon: , - value: kb.chat?.modelName + (kb.chat?.provider ? ` (${kb.chat.provider})` : "") || "无", - }, - ] : []), + ...(showModelFields + ? [ + { + label: "索引模型", + key: "embeddingModel", + icon: , + value: + kb.embedding?.modelName + + (kb.embedding?.provider + ? ` (${kb.embedding.provider})` + : "") || "无", + }, + { + label: "文本理解模型", + key: "chatModel", + icon: , + value: + kb.chat?.modelName + + (kb.chat?.provider ? ` (${kb.chat.provider})` : "") || "无", + }, + ] + : []), { label: "文件数", key: "fileCount", @@ -114,4 +125,26 @@ export function mapFileData(file: Partial): KBFile { color: "#d9d9d9", }, }; -} \ No newline at end of file +} + +export const DatasetFileCols = [ + { + title: "所属数据集", + dataIndex: "datasetName", + key: "datasetName", + ellipsis: true, + }, + { + title: "文件名", + dataIndex: "fileName", + key: "fileName", + ellipsis: true, + }, + { + title: "大小", + dataIndex: "fileSize", + key: "fileSize", + ellipsis: true, + render: formatBytes, + }, +]; diff --git a/frontend/src/pages/SynthesisTask/CreateTask.tsx b/frontend/src/pages/SynthesisTask/CreateTask.tsx index 6b4e9fe..5a1d7dd 100644 --- a/frontend/src/pages/SynthesisTask/CreateTask.tsx +++ b/frontend/src/pages/SynthesisTask/CreateTask.tsx @@ -37,8 +37,7 @@ import { } from "lucide-react"; import { Link, useNavigate } from "react-router"; import { queryDatasetsUsingGet } from "../DataManagement/dataset.api"; -import { formatBytes } from "@/utils/unit"; -import DatasetFileTransfer from "../KnowledgeBase/components/DatasetFileTransfer"; +import DatasetFileTransfer from "../../components/DatasetFileTransfer"; const { TextArea } = Input;