You've already forked DataMate
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:
248
frontend/src/components/DatasetFileTransfer.tsx
Normal file
248
frontend/src/components/DatasetFileTransfer.tsx
Normal file
@@ -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<HTMLDivElement> {
|
||||||
|
open: boolean;
|
||||||
|
selectedFilesMap: { [key: string]: DatasetFile };
|
||||||
|
onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customize Table Transfer
|
||||||
|
const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
|
||||||
|
open,
|
||||||
|
selectedFilesMap,
|
||||||
|
onSelectedFilesChange,
|
||||||
|
...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: 10, total: 0 });
|
||||||
|
|
||||||
|
const [files, setFiles] = React.useState<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 [showFiles, setShowFiles] = React.useState<boolean>(false);
|
||||||
|
const [selectedDataset, setSelectedDataset] = React.useState<Dataset | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [datasetSelections, setDatasetSelections] = React.useState<Dataset[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div {...props}>
|
||||||
|
<div className="grid grid-cols-25 gap-4 w-full">
|
||||||
|
<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}
|
||||||
|
allowClear
|
||||||
|
onChange={(e) => setDatasetSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
scroll={{ y: 400 }}
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
rowClassName={(record) =>
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<RightOutlined />
|
||||||
|
<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={filesSearch}
|
||||||
|
onChange={(e) => setFilesSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
scroll={{ y: 400 }}
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
dataSource={files}
|
||||||
|
columns={fileCols.slice(1, fileCols.length)}
|
||||||
|
pagination={filesPagination}
|
||||||
|
onRow={(record: DatasetFile) => ({
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button className="mt-4" onClick={() => setShowFiles(!showFiles)}>
|
||||||
|
{showFiles ? "取消预览" : "预览"}
|
||||||
|
</Button>
|
||||||
|
<div hidden={!showFiles}>
|
||||||
|
<Table
|
||||||
|
scroll={{ y: 400 }}
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
dataSource={Object.values(selectedFilesMap)}
|
||||||
|
columns={fileCols}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatasetFileTransfer;
|
||||||
@@ -6,6 +6,7 @@ import { datasetTypeMap } from "../../dataset.const";
|
|||||||
export default function Overview({ dataset, filesOperation }) {
|
export default function Overview({ dataset, filesOperation }) {
|
||||||
const {
|
const {
|
||||||
fileList,
|
fileList,
|
||||||
|
pagination,
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
setSelectedFiles,
|
setSelectedFiles,
|
||||||
previewVisible,
|
previewVisible,
|
||||||
@@ -179,7 +180,10 @@ export default function Overview({ dataset, filesOperation }) {
|
|||||||
dataSource={fileList}
|
dataSource={fileList}
|
||||||
// rowSelection={rowSelection}
|
// rowSelection={rowSelection}
|
||||||
scroll={{ x: "max-content", y: 600 }}
|
scroll={{ x: "max-content", y: 600 }}
|
||||||
pagination={{ showTotal: (total) => `共 ${total} 条` }}
|
pagination={{
|
||||||
|
...pagination,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export function useFilesOperation(dataset: Dataset) {
|
|||||||
// 文件相关状态
|
// 文件相关状态
|
||||||
const [fileList, setFileList] = useState<DatasetFile[]>([]);
|
const [fileList, setFileList] = useState<DatasetFile[]>([]);
|
||||||
const [selectedFiles, setSelectedFiles] = useState<number[]>([]);
|
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);
|
const [previewVisible, setPreviewVisible] = useState(false);
|
||||||
@@ -26,7 +31,10 @@ export function useFilesOperation(dataset: Dataset) {
|
|||||||
const [previewFileName, setPreviewFileName] = useState("");
|
const [previewFileName, setPreviewFileName] = useState("");
|
||||||
|
|
||||||
const fetchFiles = async () => {
|
const fetchFiles = async () => {
|
||||||
const { data } = await queryDatasetFilesUsingGet(id!);
|
const { data } = await queryDatasetFilesUsingGet(id!, {
|
||||||
|
page: pagination.current - 1,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
});
|
||||||
setFileList(data.content || []);
|
setFileList(data.content || []);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,6 +113,7 @@ export function useFilesOperation(dataset: Dataset) {
|
|||||||
fileList,
|
fileList,
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
setSelectedFiles,
|
setSelectedFiles,
|
||||||
|
setPagination,
|
||||||
previewVisible,
|
previewVisible,
|
||||||
setPreviewVisible,
|
setPreviewVisible,
|
||||||
previewContent,
|
previewContent,
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
Steps,
|
Steps,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
|
Table,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
||||||
import DatasetFileTransfer from "./DatasetFileTransfer";
|
import DatasetFileTransfer from "../../../components/DatasetFileTransfer";
|
||||||
import { DescriptionsItemType } from "antd/es/descriptions";
|
import { DescriptionsItemType } from "antd/es/descriptions";
|
||||||
import { DatasetFile } from "@/pages/DataManagement/dataset.model";
|
import { DatasetFileCols } from "../knowledge-base.const";
|
||||||
|
|
||||||
const sliceOptions = [
|
const sliceOptions = [
|
||||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||||
@@ -29,9 +30,7 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
|
||||||
const [selectedMap, setSelectedMap] = useState<Record<string, DatasetFile[]>>(
|
const [selectedFilesMap, setSelectedFilesMap] = useState({});
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 定义分块选项
|
// 定义分块选项
|
||||||
const sliceOptions = [
|
const sliceOptions = [
|
||||||
@@ -67,8 +66,8 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
|
|
||||||
// 获取已选择文件总数
|
// 获取已选择文件总数
|
||||||
const getSelectedFilesCount = () => {
|
const getSelectedFilesCount = () => {
|
||||||
return Object.values(selectedMap).reduce(
|
return Object.values(selectedFilesMap).reduce(
|
||||||
(total, files) => total + files.length,
|
(total, ids) => total + ids.length,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -117,23 +116,13 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
delimiter: "",
|
delimiter: "",
|
||||||
});
|
});
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setSelectedFilesMap({});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddData = async () => {
|
const handleAddData = async () => {
|
||||||
const files = [];
|
const selectedFiles = [];
|
||||||
|
|
||||||
Object.entries(selectedMap).forEach(([datasetId, fileList]) => {
|
if (selectedFiles.length === 0) {
|
||||||
files.push(
|
|
||||||
...fileList.map((file) => ({
|
|
||||||
...file,
|
|
||||||
id: file.id,
|
|
||||||
name: file.fileName,
|
|
||||||
datasetId,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
message.warning("请至少选择一个文件");
|
message.warning("请至少选择一个文件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -141,7 +130,7 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
try {
|
try {
|
||||||
// 构造符合API要求的请求数据
|
// 构造符合API要求的请求数据
|
||||||
const requestData = {
|
const requestData = {
|
||||||
files,
|
files: Object.entries(selectedFilesMap),
|
||||||
processType: newKB.processType,
|
processType: newKB.processType,
|
||||||
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
||||||
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
||||||
@@ -155,7 +144,6 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
|
|
||||||
message.success("数据添加成功");
|
message.success("数据添加成功");
|
||||||
// 重置状态
|
// 重置状态
|
||||||
handleReset();
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error("数据添加失败,请重试");
|
message.error("数据添加失败,请重试");
|
||||||
@@ -164,7 +152,6 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleModalCancel = () => {
|
const handleModalCancel = () => {
|
||||||
handleReset();
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,15 +166,10 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
key: "dataSource",
|
key: "dataSource",
|
||||||
children: "数据集",
|
children: "数据集",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "选择的数据集数",
|
|
||||||
key: "selectedDatasetCount",
|
|
||||||
children: Object.keys(selectedMap).length,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "文件总数",
|
label: "文件总数",
|
||||||
key: "totalFileCount",
|
key: "totalFileCount",
|
||||||
children: getSelectedFilesCount(),
|
children: Object.keys(selectedFilesMap).length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "分块方式",
|
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 (
|
return (
|
||||||
@@ -221,7 +217,10 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => {
|
||||||
|
handleReset();
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
添加数据
|
添加数据
|
||||||
</Button>
|
</Button>
|
||||||
@@ -231,13 +230,25 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
onCancel={handleModalCancel}
|
onCancel={handleModalCancel}
|
||||||
footer={
|
footer={
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
|
{currentStep === 0 && (
|
||||||
|
<Button onClick={handleModalCancel}>取消</Button>
|
||||||
|
)}
|
||||||
{currentStep > 0 && (
|
{currentStep > 0 && (
|
||||||
<Button disabled={false} onClick={handlePrev}>
|
<Button disabled={false} onClick={handlePrev}>
|
||||||
上一步
|
上一步
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{currentStep < steps.length - 1 ? (
|
{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>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
@@ -259,12 +270,13 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 步骤内容 */}
|
{/* 步骤内容 */}
|
||||||
<DatasetFileTransfer
|
{currentStep === 0 && (
|
||||||
hidden={currentStep !== 0}
|
<DatasetFileTransfer
|
||||||
open={open}
|
open={open}
|
||||||
selectedMap={selectedMap}
|
selectedFilesMap={selectedFilesMap}
|
||||||
onSelectedChange={setSelectedMap}
|
onSelectedFilesChange={setSelectedFilesMap}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
hidden={currentStep !== 1}
|
hidden={currentStep !== 1}
|
||||||
@@ -328,9 +340,9 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div className="space-y-6" hidden={currentStep !== 2}>
|
<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>
|
<div className="text-lg font-medium mb-3">上传信息确认</div>
|
||||||
<Descriptions items={descItems} />
|
<Descriptions layout="vertical" size="small" items={descItems} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-yellow-600">
|
<div className="text-sm text-yellow-600">
|
||||||
提示:上传后系统将自动处理文件,请耐心等待
|
提示:上传后系统将自动处理文件,请耐心等待
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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 {
|
return {
|
||||||
...kb,
|
...kb,
|
||||||
icon: <BookOpenText className="w-full h-full" />,
|
icon: <BookOpenText className="w-full h-full" />,
|
||||||
description: kb.description,
|
description: kb.description,
|
||||||
statistics: [
|
statistics: [
|
||||||
...(showModelFields ? [
|
...(showModelFields
|
||||||
{
|
? [
|
||||||
label: "索引模型",
|
{
|
||||||
key: "embeddingModel",
|
label: "索引模型",
|
||||||
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
|
key: "embeddingModel",
|
||||||
value: kb.embedding?.modelName + (kb.embedding?.provider ? ` (${kb.embedding.provider})` : "") || "无",
|
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
|
||||||
},
|
value:
|
||||||
{
|
kb.embedding?.modelName +
|
||||||
label: "文本理解模型",
|
(kb.embedding?.provider
|
||||||
key: "chatModel",
|
? ` (${kb.embedding.provider})`
|
||||||
icon: <BookType className="w-4 h-4 text-blue-500" />,
|
: "") || "无",
|
||||||
value: kb.chat?.modelName + (kb.chat?.provider ? ` (${kb.chat.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: "文件数",
|
label: "文件数",
|
||||||
key: "fileCount",
|
key: "fileCount",
|
||||||
@@ -114,4 +125,26 @@ export function mapFileData(file: Partial<KBFile>): KBFile {
|
|||||||
color: "#d9d9d9",
|
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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link, useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import { queryDatasetsUsingGet } from "../DataManagement/dataset.api";
|
import { queryDatasetsUsingGet } from "../DataManagement/dataset.api";
|
||||||
import { formatBytes } from "@/utils/unit";
|
import DatasetFileTransfer from "../../components/DatasetFileTransfer";
|
||||||
import DatasetFileTransfer from "../KnowledgeBase/components/DatasetFileTransfer";
|
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user