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
This commit is contained in:
chenghh-9609
2025-11-21 11:39:26 +08:00
committed by GitHub
parent cddfe9b149
commit fdfcfec1f1
7 changed files with 364 additions and 312 deletions

View File

@@ -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}`,
}}
/>
</div>
</div>

View File

@@ -19,6 +19,11 @@ export function useFilesOperation(dataset: Dataset) {
// 文件相关状态
const [fileList, setFileList] = useState<DatasetFile[]>([]);
const [selectedFiles, setSelectedFiles] = useState<number[]>([]);
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,

View File

@@ -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<Record<string, DatasetFile[]>>(
{}
);
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: (
<Table
scroll={{ y: 400 }}
rowKey="id"
size="small"
dataSource={Object.values(selectedFilesMap)}
columns={DatasetFileCols}
/>
),
},
];
return (
@@ -221,7 +217,10 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setOpen(true)}
onClick={() => {
handleReset();
setOpen(true);
}}
>
</Button>
@@ -231,13 +230,25 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
onCancel={handleModalCancel}
footer={
<div className="space-x-2">
{currentStep === 0 && (
<Button onClick={handleModalCancel}></Button>
)}
{currentStep > 0 && (
<Button disabled={false} onClick={handlePrev}>
</Button>
)}
{currentStep < steps.length - 1 ? (
<Button type="primary" onClick={handleNext}>
<Button
type="primary"
disabled={
Object.keys(selectedFilesMap).length === 0 ||
!newKB.chunkSize ||
!newKB.overlapSize ||
!newKB.processType
}
onClick={handleNext}
>
</Button>
) : (
@@ -259,12 +270,13 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
/>
{/* 步骤内容 */}
<DatasetFileTransfer
hidden={currentStep !== 0}
open={open}
selectedMap={selectedMap}
onSelectedChange={setSelectedMap}
/>
{currentStep === 0 && (
<DatasetFileTransfer
open={open}
selectedFilesMap={selectedFilesMap}
onSelectedFilesChange={setSelectedFilesMap}
/>
)}
<Form
hidden={currentStep !== 1}
@@ -328,9 +340,9 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
</Form>
<div className="space-y-6" hidden={currentStep !== 2}>
<div className="p-4 bg-blue-50 rounded-lg">
<div className="">
<div className="text-lg font-medium mb-3"></div>
<Descriptions items={descItems} />
<Descriptions layout="vertical" size="small" items={descItems} />
</div>
<div className="text-sm text-yellow-600">

View File

@@ -1,253 +0,0 @@
import React, { useEffect } from "react";
import { 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";
interface DatasetFileTransferProps
extends React.HTMLAttributes<HTMLDivElement> {
open: boolean;
selectedMap: Record<string, DatasetFile[]>;
onSelectedChange: (filesMap: Record<string, DatasetFile[]>) => void;
}
// Customize Table Transfer
const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
open,
selectedMap,
onSelectedChange,
...props
}) => {
const [datasets, setDatasets] = React.useState<Dataset[]>([]);
const [datasetSearch, setDatasetSearch] = React.useState<string>("");
const [datasetPagination, setDatasetPagination] = React.useState<{
current: number;
pageSize: number;
total: number;
}>({ current: 1, pageSize: 1000, total: 0 });
const [expandedRowKeys, setExpandedRowKeys] = React.useState<React.Key[]>([]);
const [loadedFiles, setLoadedFiles] = React.useState<
Record<string, DatasetFile[]>
>({});
const [filesSearch, setFilesSearch] = React.useState<string>("");
const [filesPagination, setFilesPagination] = React.useState<{
current: number;
pageSize: number;
total: number;
}>({ current: 1, pageSize: 10, total: 0 });
const selectedFiles = React.useMemo(() => {
const files: DatasetFile[] = [];
Object.values(selectedMap).forEach((fileList) => {
files.push(...fileList);
});
return files;
}, [selectedMap]);
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,
}));
};
useEffect(() => {
if (open) {
fetchDatasets();
}
}, [open]);
const fetchFiles = async (dataset: Dataset) => {
if (!dataset || loadedFiles[dataset.id]) return;
const { data } = await queryDatasetFilesUsingGet(dataset.id, {
page: filesPagination.current - 1,
size: 1000,
keyword: filesSearch,
});
setLoadedFiles((prev) => ({
...prev,
[dataset.id]: data.content,
}));
setFilesPagination((prev) => ({
...prev,
total: data.totalElements,
}));
return data.content;
};
const onExpand = (expanded: boolean, record: Dataset) => {
if (expanded) {
fetchFiles(record);
setExpandedRowKeys([...expandedRowKeys, record.id]);
} else {
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.id));
}
};
const toggleSelectFile = (dataset: Dataset, record: DatasetFile) => {
const datasetFiles = selectedMap[dataset.id] || [];
const hasSelected = datasetFiles.find((file) => file.id === record.id);
let files = [...datasetFiles];
if (!hasSelected) {
files.push(record);
} else {
files = datasetFiles.filter((file) => file.id !== record.id);
}
const newMap = { ...selectedMap, [dataset.id]: files };
if (files.length === 0) {
delete newMap[dataset.id];
}
onSelectedChange(newMap);
};
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,
},
];
const fileCols = [
{
title: "文件名",
dataIndex: "fileName",
key: "fileName",
ellipsis: true,
},
{
title: "大小",
dataIndex: "size",
key: "size",
ellipsis: true,
render: formatBytes,
},
];
return (
<div className="grid grid-cols-25 gap-4 w-full" {...props}>
<div className="border-card flex flex-col col-span-12">
<div className="border-bottom p-2 font-bold"></div>
<div className="p-2">
<Input
placeholder="搜索数据集名称..."
value={datasetSearch}
onChange={(e) => setDatasetSearch(e.target.value)}
/>
</div>
<Table
scroll={{ y: 400 }}
rowKey="id"
size="small"
onRow={(record: Dataset) => ({
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) => (
<Table
scroll={{ y: 400 }}
rowKey="id"
size="small"
dataSource={loadedFiles[dataset.id] || []}
columns={fileCols}
pagination={filesPagination}
onRow={(record: DatasetFile) => ({
onClick: () => toggleSelectFile(dataset, record),
})}
rowSelection={{
type: "checkbox",
selectedRowKeys: Object.values(
selectedMap[dataset.id] || {}
).map((file) => file.id),
onSelect: (record) => toggleSelectFile(dataset, record),
}}
/>
),
}}
/>
</div>
<RightOutlined />
<div className="border-card flex flex-col col-span-12">
<div className="border-bottom p-2 font-bold">
{selectedFiles.length}
</div>
<div className="p-2">
<Input
placeholder="搜索文件名称..."
value={filesSearch}
onChange={(e) => setFilesSearch(e.target.value)}
/>
</div>
<Table
size="small"
scroll={{ y: 400 }}
rowKey="id"
dataSource={selectedFiles}
columns={fileCols}
/>
</div>
</div>
);
};
export default DatasetFileTransfer;

View File

@@ -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: <BookOpenText className="w-full h-full" />,
description: kb.description,
statistics: [
...(showModelFields ? [
{
label: "索引模型",
key: "embeddingModel",
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
value: kb.embedding?.modelName + (kb.embedding?.provider ? ` (${kb.embedding.provider})` : "") || "无",
},
{
label: "文本理解模型",
key: "chatModel",
icon: <BookType className="w-4 h-4 text-blue-500" />,
value: kb.chat?.modelName + (kb.chat?.provider ? ` (${kb.chat.provider})` : "") || "无",
},
] : []),
...(showModelFields
? [
{
label: "索引模型",
key: "embeddingModel",
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
value:
kb.embedding?.modelName +
(kb.embedding?.provider
? ` (${kb.embedding.provider})`
: "") || "无",
},
{
label: "文本理解模型",
key: "chatModel",
icon: <BookType className="w-4 h-4 text-blue-500" />,
value:
kb.chat?.modelName +
(kb.chat?.provider ? ` (${kb.chat.provider})` : "") || "无",
},
]
: []),
{
label: "文件数",
key: "fileCount",
@@ -114,4 +125,26 @@ export function mapFileData(file: Partial<KBFile>): KBFile {
color: "#d9d9d9",
},
};
}
}
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,
},
];

View File

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