You've already forked DataMate
add select dataset files component (#94)
* feat: Refactor AddDataDialog and introduce DatasetFileTransfer component for improved file selection and management * feat: Refactor SynthesisTask and InstructionTemplate components for improved UI and functionality; integrate DatasetFileTransfer for file management * feat: Enhance CollectionTaskCreate form with additional fields for MYSQL configuration and prefix input
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
App,
|
App,
|
||||||
@@ -7,31 +7,13 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
Modal,
|
Modal,
|
||||||
Steps,
|
Steps,
|
||||||
Empty,
|
Descriptions,
|
||||||
Checkbox,
|
|
||||||
Pagination,
|
|
||||||
Space,
|
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { SearchOutlined, PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
|
||||||
import {
|
|
||||||
queryDatasetFilesUsingGet,
|
|
||||||
queryDatasetsUsingGet,
|
|
||||||
} from "@/pages/DataManagement/dataset.api";
|
|
||||||
import { datasetTypeMap } from "@/pages/DataManagement/dataset.const";
|
|
||||||
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
||||||
import { DatasetType } from "@/pages/DataManagement/dataset.model";
|
import DatasetFileTransfer from "./DatasetFileTransfer";
|
||||||
|
import { DescriptionsItemType } from "antd/es/descriptions";
|
||||||
// 定义简单的FileInfo接口,确保与API兼容
|
import { DatasetFile } from "@/pages/DataManagement/dataset.model";
|
||||||
interface FileInfo {
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
fileName?: string;
|
|
||||||
size?: string;
|
|
||||||
createdAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Step } = Steps;
|
|
||||||
|
|
||||||
const sliceOptions = [
|
const sliceOptions = [
|
||||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||||
@@ -42,34 +24,22 @@ const sliceOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
// 数据集相关状态
|
|
||||||
const [datasets, setDatasets] = useState<any[]>([]);
|
const [selectedMap, setSelectedMap] = useState<Record<string, DatasetFile[]>>(
|
||||||
const [datasetsTotal, setDatasetsTotal] = useState(0);
|
{}
|
||||||
const [datasetPage, setDatasetPage] = useState(1);
|
);
|
||||||
const [datasetSearch, setDatasetSearch] = useState('');
|
|
||||||
const [datasetsLoading, setDatasetsLoading] = useState(false);
|
|
||||||
// 文件相关状态
|
|
||||||
const [datasetFiles, setDatasetFiles] = useState<any[]>([]);
|
|
||||||
const [filesTotal, setFilesTotal] = useState(0);
|
|
||||||
const [filesPage, setFilesPage] = useState(0);
|
|
||||||
const [fileSearch, setFileSearch] = useState('knowledge-base/detail/');
|
|
||||||
const [filesLoading, setFilesLoading] = useState(false);
|
|
||||||
// 已选择的文件,格式:{datasetId: [fileIds]}
|
|
||||||
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, string[]>>({});
|
|
||||||
// 当前正在查看的数据集
|
|
||||||
const [activeDataset, setActiveDataset] = useState<any>(null);
|
|
||||||
|
|
||||||
// 定义分块选项
|
// 定义分块选项
|
||||||
const sliceOptions = [
|
const sliceOptions = [
|
||||||
{ label: '默认分块', value: 'DEFAULT_CHUNK' },
|
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||||
{ label: '按章节分块', value: 'CHAPTER_CHUNK' },
|
{ label: "按章节分块", value: "CHAPTER_CHUNK" },
|
||||||
{ label: '按段落分块', value: 'PARAGRAPH_CHUNK' },
|
{ label: "按段落分块", value: "PARAGRAPH_CHUNK" },
|
||||||
{ label: '固定长度分块', value: 'FIXED_LENGTH_CHUNK' },
|
{ label: "固定长度分块", value: "FIXED_LENGTH_CHUNK" },
|
||||||
{ label: '自定义分隔符分块', value: 'CUSTOM_SEPARATOR_CHUNK' },
|
{ label: "自定义分隔符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 定义初始状态
|
// 定义初始状态
|
||||||
@@ -77,224 +47,56 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
processType: "DEFAULT_CHUNK",
|
processType: "DEFAULT_CHUNK",
|
||||||
chunkSize: 500,
|
chunkSize: 500,
|
||||||
overlapSize: 50,
|
overlapSize: 50,
|
||||||
delimiter: '',
|
delimiter: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
title: '选择数据集文件',
|
title: "选择数据集文件",
|
||||||
description: '从多个数据集中选择文件',
|
description: "从多个数据集中选择文件",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '配置参数',
|
title: "配置参数",
|
||||||
description: '设置数据处理参数',
|
description: "设置数据处理参数",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '确认上传',
|
title: "确认上传",
|
||||||
description: '确认信息并上传',
|
description: "确认信息并上传",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 数据集列表每页大小
|
|
||||||
const DATASET_PAGE_SIZE = 6;
|
|
||||||
|
|
||||||
// 获取数据集列表(支持分页和搜索)
|
|
||||||
const fetchDatasets = async (page = 1, search = '') => {
|
|
||||||
setDatasetsLoading(true);
|
|
||||||
try {
|
|
||||||
const { data } = await queryDatasetsUsingGet({
|
|
||||||
page: page,
|
|
||||||
size: DATASET_PAGE_SIZE, // 每页大小通过接口传递
|
|
||||||
type: DatasetType.TEXT,
|
|
||||||
keyword: search || undefined, // 搜索参数
|
|
||||||
});
|
|
||||||
setDatasets(data.content || []);
|
|
||||||
setDatasetsTotal(data.totalElements || 0);
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取数据集失败');
|
|
||||||
console.error('获取数据集列表失败:', error);
|
|
||||||
} finally {
|
|
||||||
setDatasetsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 文件列表每页大小
|
|
||||||
const FILES_PAGE_SIZE = 8;
|
|
||||||
|
|
||||||
// 获取数据集文件列表(支持分页和搜索)
|
|
||||||
const fetchDatasetFiles = async (datasetId, page = 0, search = '') => {
|
|
||||||
if (!datasetId) return;
|
|
||||||
|
|
||||||
setFilesLoading(true);
|
|
||||||
try {
|
|
||||||
const { data } = await queryDatasetFilesUsingGet(datasetId, {
|
|
||||||
page: page, // 后端使用0-based页码
|
|
||||||
size: FILES_PAGE_SIZE, // 每页最多8条数据
|
|
||||||
keyword: search || undefined, // 搜索参数
|
|
||||||
});
|
|
||||||
|
|
||||||
// 确保数据格式正确
|
|
||||||
if (data && Array.isArray(data.content)) {
|
|
||||||
setDatasetFiles(data.content || []);
|
|
||||||
setFilesTotal(data.totalElements || 0);
|
|
||||||
} else {
|
|
||||||
setDatasetFiles([]);
|
|
||||||
setFilesTotal(0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取数据集文件失败');
|
|
||||||
console.error('获取文件列表失败:', error);
|
|
||||||
} finally {
|
|
||||||
setFilesLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化时加载数据集
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen && currentStep === 0) {
|
|
||||||
fetchDatasets(datasetPage, datasetSearch);
|
|
||||||
}
|
|
||||||
}, [isOpen, currentStep, datasetPage, datasetSearch]);
|
|
||||||
|
|
||||||
// 切换数据集时加载对应文件(重置页码为0)
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeDataset) {
|
|
||||||
setFilesPage(0); // 重置页码
|
|
||||||
fetchDatasetFiles(activeDataset.id, 0, fileSearch);
|
|
||||||
}
|
|
||||||
}, [activeDataset, fileSearch]);
|
|
||||||
|
|
||||||
// 确保在文件搜索文本变化时重新加载文件
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeDataset && fileSearch !== undefined) {
|
|
||||||
setFilesPage(0);
|
|
||||||
fetchDatasetFiles(activeDataset.id, 0, fileSearch);
|
|
||||||
}
|
|
||||||
}, [fileSearch, activeDataset]);
|
|
||||||
|
|
||||||
// 当文件页码变化时重新加载文件
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeDataset && filesPage >= 1) {
|
|
||||||
fetchDatasetFiles(activeDataset.id, filesPage, fileSearch);
|
|
||||||
}
|
|
||||||
}, [filesPage]);
|
|
||||||
|
|
||||||
// 处理数据集搜索
|
|
||||||
const handleDatasetSearch = () => {
|
|
||||||
setDatasetPage(1);
|
|
||||||
fetchDatasets(1, datasetSearch);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理文件搜索
|
|
||||||
const handleFileSearch = (value) => {
|
|
||||||
setFileSearch(value);
|
|
||||||
setFilesPage(0);
|
|
||||||
if (activeDataset) {
|
|
||||||
fetchDatasetFiles(activeDataset.id, 0, value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理数据集分页变化
|
|
||||||
const handleDatasetPageChange = (page) => {
|
|
||||||
setDatasetPage(page);
|
|
||||||
fetchDatasets(page, datasetSearch);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理文件分页变化
|
|
||||||
const handleFilesPageChange = (page) => {
|
|
||||||
setFilesPage(page);
|
|
||||||
if (activeDataset) {
|
|
||||||
fetchDatasetFiles(activeDataset.id, page, fileSearch);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 切换活动数据集
|
|
||||||
const handleDatasetClick = (dataset) => {
|
|
||||||
setActiveDataset(dataset);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 已经在后面定义了handleFileSelect函数,删除重复定义
|
|
||||||
|
|
||||||
// 处理全选/取消全选
|
|
||||||
const handleSelectAll = (e) => {
|
|
||||||
if (!activeDataset) return;
|
|
||||||
|
|
||||||
const newSelectedFiles = e.target.checked
|
|
||||||
? datasetFiles.map(file => file.id)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
setSelectedFilesMap(prev => ({
|
|
||||||
...prev,
|
|
||||||
[activeDataset.id]: newSelectedFiles
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查文件是否已选择
|
|
||||||
const isFileSelected = (fileId) => {
|
|
||||||
if (!activeDataset) return false;
|
|
||||||
return selectedFilesMap[activeDataset.id]?.includes(fileId) || false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查当前数据集是否全选
|
|
||||||
const isAllSelected = () => {
|
|
||||||
if (!activeDataset || datasetFiles.length === 0) return false;
|
|
||||||
const selectedCount = selectedFilesMap[activeDataset.id]?.length || 0;
|
|
||||||
return selectedCount === datasetFiles.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查是否部分选择
|
|
||||||
const isIndeterminate = () => {
|
|
||||||
if (!activeDataset) return false;
|
|
||||||
const selectedCount = selectedFilesMap[activeDataset.id]?.length || 0;
|
|
||||||
return selectedCount > 0 && selectedCount < datasetFiles.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取已选择文件总数
|
// 获取已选择文件总数
|
||||||
const getSelectedFilesCount = () => {
|
const getSelectedFilesCount = () => {
|
||||||
return Object.values(selectedFilesMap).reduce((total, ids) => total + ids.length, 0);
|
return Object.values(selectedMap).reduce(
|
||||||
};
|
(total, files) => total + files.length,
|
||||||
|
0
|
||||||
// 获取所有已选择的文件信息
|
);
|
||||||
const getAllSelectedFiles = () => {
|
|
||||||
const allFiles = [];
|
|
||||||
for (const [datasetId, fileIds] of Object.entries(selectedFilesMap)) {
|
|
||||||
// 找到对应的数据集
|
|
||||||
const dataset = datasets.find(d => d.id === datasetId);
|
|
||||||
// 获取文件详情并添加到数组
|
|
||||||
if (dataset) {
|
|
||||||
allFiles.push(...fileIds.map(fileId => ({
|
|
||||||
id: fileId,
|
|
||||||
name: `${dataset.name}/file_${fileId}` // 使用数据集名称作为前缀
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return allFiles;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
// 验证当前步骤
|
// 验证当前步骤
|
||||||
if (currentStep === 0) {
|
if (currentStep === 0) {
|
||||||
if (getSelectedFilesCount() === 0) {
|
if (getSelectedFilesCount() === 0) {
|
||||||
message.warning('请至少选择一个文件');
|
message.warning("请至少选择一个文件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentStep === 1) {
|
if (currentStep === 1) {
|
||||||
// 验证切片参数
|
// 验证切片参数
|
||||||
if (!newKB.processType) {
|
if (!newKB.processType) {
|
||||||
message.warning('请选择分块方式');
|
message.warning("请选择分块方式");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!newKB.chunkSize || Number(newKB.chunkSize) <= 0) {
|
if (!newKB.chunkSize || Number(newKB.chunkSize) <= 0) {
|
||||||
message.warning('请输入有效的分块大小');
|
message.warning("请输入有效的分块大小");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!newKB.overlapSize || Number(newKB.overlapSize) < 0) {
|
if (!newKB.overlapSize || Number(newKB.overlapSize) < 0) {
|
||||||
message.warning('请输入有效的重叠长度');
|
message.warning("请输入有效的重叠长度");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newKB.processType === 'CUSTOM_SEPARATOR_CHUNK' && !newKB.delimiter) {
|
if (newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && !newKB.delimiter) {
|
||||||
message.warning('请输入分隔符');
|
message.warning("请输入分隔符");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,93 +110,38 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
// 重置所有状态
|
// 重置所有状态
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setCurrentStep(0);
|
setCurrentStep(0);
|
||||||
setSelectedFilesMap({});
|
|
||||||
setNewKB({
|
setNewKB({
|
||||||
processType: 'DEFAULT_CHUNK',
|
processType: "DEFAULT_CHUNK",
|
||||||
chunkSize: 500,
|
chunkSize: 500,
|
||||||
overlapSize: 50,
|
overlapSize: 50,
|
||||||
delimiter: '',
|
delimiter: "",
|
||||||
});
|
});
|
||||||
setDatasets([]);
|
|
||||||
setDatasetPage(1);
|
|
||||||
setDatasetSearch('');
|
|
||||||
setDatasetFiles([]);
|
|
||||||
setFilesPage(1);
|
|
||||||
setFileSearch('');
|
|
||||||
setActiveDataset(null);
|
|
||||||
setLoadedFilesCache({}); // 清除文件缓存
|
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 用于缓存已加载过的文件信息,避免重复请求
|
|
||||||
const [loadedFilesCache, setLoadedFilesCache] = useState<Record<string, Record<string, any>>>({});
|
|
||||||
|
|
||||||
// 优化处理文件选择(确保选择状态在分页切换后保持)
|
|
||||||
const handleFileSelect = (checkedValues) => {
|
|
||||||
if (!activeDataset) return;
|
|
||||||
|
|
||||||
// 更新选择的文件
|
|
||||||
setSelectedFilesMap(prev => ({
|
|
||||||
...prev,
|
|
||||||
[activeDataset.id]: checkedValues
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 缓存当前页面的文件信息
|
|
||||||
if (datasetFiles.length > 0) {
|
|
||||||
setLoadedFilesCache(prev => {
|
|
||||||
const datasetCache = prev[activeDataset.id] || {};
|
|
||||||
// 更新缓存中的文件信息
|
|
||||||
datasetFiles.forEach(file => {
|
|
||||||
datasetCache[file.id] = file;
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[activeDataset.id]: datasetCache
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 当数据集文件加载完成后,缓存文件信息
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeDataset && datasetFiles.length > 0) {
|
|
||||||
setLoadedFilesCache(prev => {
|
|
||||||
const datasetCache = prev[activeDataset.id] || {};
|
|
||||||
datasetFiles.forEach(file => {
|
|
||||||
datasetCache[file.id] = file;
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[activeDataset.id]: datasetCache
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [activeDataset, datasetFiles]);
|
|
||||||
|
|
||||||
const handleAddData = async () => {
|
const handleAddData = async () => {
|
||||||
const selectedFiles = [];
|
const files = [];
|
||||||
|
|
||||||
Object.entries(selectedFilesMap).forEach(([datasetId, fileIds]) => {
|
Object.entries(selectedMap).forEach(([datasetId, fileList]) => {
|
||||||
fileIds.forEach(fileId => {
|
files.push(
|
||||||
// 查找文件信息以获取文件名
|
...fileList.map((file) => ({
|
||||||
const fileInfo = datasetFiles.find(file => file.id === fileId);
|
...file,
|
||||||
// 根据API定义,需要id和name字段
|
id: file.id,
|
||||||
selectedFiles.push({
|
name: file.fileName,
|
||||||
id: fileId,
|
datasetId,
|
||||||
name: fileInfo?.name || fileInfo?.fileName || `文件_${fileId}`
|
}))
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedFiles.length === 0) {
|
if (files.length === 0) {
|
||||||
message.warning('请至少选择一个文件');
|
message.warning("请至少选择一个文件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 构造符合API要求的请求数据
|
// 构造符合API要求的请求数据
|
||||||
const requestData = {
|
const requestData = {
|
||||||
files: selectedFiles,
|
files,
|
||||||
processType: newKB.processType,
|
processType: newKB.processType,
|
||||||
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
||||||
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
||||||
@@ -406,396 +153,188 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
|||||||
// 先通知父组件刷新数据(确保刷新发生在重置前)
|
// 先通知父组件刷新数据(确保刷新发生在重置前)
|
||||||
onDataAdded?.();
|
onDataAdded?.();
|
||||||
|
|
||||||
message.success('数据添加成功');
|
message.success("数据添加成功");
|
||||||
// 重置状态
|
// 重置状态
|
||||||
handleReset();
|
handleReset();
|
||||||
setIsOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('数据添加失败,请重试');
|
message.error("数据添加失败,请重试");
|
||||||
console.error('添加文件失败:', error);
|
console.error("添加文件失败:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// handleReset函数已在前面定义,删除重复定义
|
|
||||||
|
|
||||||
const handleModalCancel = () => {
|
const handleModalCancel = () => {
|
||||||
handleReset();
|
handleReset();
|
||||||
setIsOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderStepContent = () => {
|
const descItems: DescriptionsItemType[] = [
|
||||||
switch (currentStep) {
|
{
|
||||||
case 0:
|
label: "知识库名称",
|
||||||
return (
|
key: "knowledgeBaseName",
|
||||||
<div className="space-y-6">
|
children: knowledgeBase?.name,
|
||||||
<div className="text-lg font-medium">选择数据集文件</div>
|
},
|
||||||
<div className="space-y-4">
|
{
|
||||||
<div className="text-sm text-gray-600">
|
label: "数据来源",
|
||||||
请从左侧选择数据集,然后在右侧选择需要导入的文件。支持从多个不同数据集中交叉选择文件。
|
key: "dataSource",
|
||||||
</div>
|
children: "数据集",
|
||||||
|
},
|
||||||
{getSelectedFilesCount() > 0 && (
|
{
|
||||||
<div className="p-3 bg-blue-50 rounded-md text-blue-700">
|
label: "选择的数据集数",
|
||||||
已选择 {getSelectedFilesCount()} 个文件(来自 {Object.keys(selectedFilesMap).length} 个数据集)
|
key: "selectedDatasetCount",
|
||||||
</div>
|
children: Object.keys(selectedMap).length,
|
||||||
)}
|
},
|
||||||
|
{
|
||||||
<div className="grid grid-cols-2 gap-4 h-[500px]">
|
label: "文件总数",
|
||||||
{/* 左侧数据集列表(带搜索和分页) */}
|
key: "totalFileCount",
|
||||||
<div className="border rounded-lg flex flex-col h-full">
|
children: getSelectedFilesCount(),
|
||||||
<div className="p-4 border-b">
|
},
|
||||||
<div className="font-medium mb-3">数据集列表</div>
|
{
|
||||||
<div className="relative">
|
label: "分块方式",
|
||||||
<Input
|
key: "chunkingMethod",
|
||||||
placeholder="搜索数据集名称"
|
children:
|
||||||
value={datasetSearch}
|
sliceOptions.find((opt) => opt.value === newKB.processType)?.label ||
|
||||||
onChange={(e) => setDatasetSearch(e.target.value)}
|
"",
|
||||||
onPressEnter={handleDatasetSearch}
|
},
|
||||||
prefix={<SearchOutlined />}
|
{
|
||||||
suffix={
|
label: "分块大小",
|
||||||
<Button type="primary" size="small" onClick={handleDatasetSearch}>
|
key: "chunkSize",
|
||||||
搜索
|
children: newKB.chunkSize,
|
||||||
</Button>
|
},
|
||||||
}
|
{
|
||||||
/>
|
label: "重叠长度",
|
||||||
</div>
|
key: "overlapSize",
|
||||||
</div>
|
children: newKB.overlapSize,
|
||||||
|
},
|
||||||
<div className="flex-1 overflow-y-auto p-4 max-h-[400px]">
|
...(newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && newKB.delimiter
|
||||||
{datasetsLoading ? (
|
? [
|
||||||
<div className="flex justify-center items-center h-full">加载中...</div>
|
{
|
||||||
) : datasets.length === 0 ? (
|
label: "分隔符",
|
||||||
<Empty description="暂无可用数据集" />
|
children: <span className="font-mono">{newKB.delimiter}</span>,
|
||||||
) : (
|
},
|
||||||
<div className="space-y-3">
|
]
|
||||||
{datasets.map(dataset => {
|
: []),
|
||||||
const isSelected = selectedFilesMap[dataset.id]?.length > 0;
|
];
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={dataset.id}
|
|
||||||
className={`p-3 rounded cursor-pointer transition-all border ${activeDataset?.id === dataset.id
|
|
||||||
? 'bg-blue-50 border-blue-300'
|
|
||||||
: isSelected
|
|
||||||
? 'border-blue-200 bg-blue-50/50'
|
|
||||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'}`}
|
|
||||||
onClick={() => handleDatasetClick(dataset)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="font-medium">{dataset.name}</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
类型: {datasetTypeMap[dataset.datasetType]?.label || '未知'}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
文件数: {dataset.fileCount}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isSelected && (
|
|
||||||
<div className="mt-1 text-xs text-blue-600">
|
|
||||||
已选择 {selectedFilesMap[dataset.id].length} 个文件
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 数据集分页 */}
|
|
||||||
<div className="p-3 border-t">
|
|
||||||
<Pagination
|
|
||||||
current={datasetPage}
|
|
||||||
total={datasetsTotal}
|
|
||||||
pageSize={DATASET_PAGE_SIZE}
|
|
||||||
onChange={handleDatasetPageChange}
|
|
||||||
showSizeChanger={false}
|
|
||||||
showQuickJumper
|
|
||||||
showTotal={(total) => `共 ${total} 个数据集`}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧文件列表(带搜索和分页) */}
|
|
||||||
<div className="border rounded-lg flex flex-col h-full">
|
|
||||||
<div className="p-4 border-b">
|
|
||||||
<div className="font-medium mb-3">
|
|
||||||
{activeDataset ? `${activeDataset.name} 的文件` : '文件列表'}
|
|
||||||
</div>
|
|
||||||
{activeDataset && (
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
placeholder="搜索文件名"
|
|
||||||
value={fileSearch}
|
|
||||||
onChange={(e) => setFileSearch(e.target.value)}
|
|
||||||
onPressEnter={handleFileSearch}
|
|
||||||
prefix={<SearchOutlined />}
|
|
||||||
suffix={
|
|
||||||
<Button type="primary" size="small" onClick={handleFileSearch}>
|
|
||||||
搜索
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4 max-h-[400px]">
|
|
||||||
{!activeDataset ? (
|
|
||||||
<Empty description="请先选择数据集" />
|
|
||||||
) : filesLoading ? (
|
|
||||||
<div className="flex justify-center items-center h-full">加载中...</div>
|
|
||||||
) : datasetFiles.length === 0 ? (
|
|
||||||
<Empty description="该数据集暂无文件" />
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-3">
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
每页显示 {Math.min(datasetFiles.length, FILES_PAGE_SIZE)} 条,共 {filesTotal} 条
|
|
||||||
</span>
|
|
||||||
<Checkbox
|
|
||||||
checked={isAllSelected()}
|
|
||||||
onChange={handleSelectAll}
|
|
||||||
indeterminate={isIndeterminate()}
|
|
||||||
>
|
|
||||||
全选
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Checkbox.Group
|
|
||||||
value={selectedFilesMap[activeDataset.id] || []}
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{datasetFiles.map(file => (
|
|
||||||
<div key={file.id} className="p-2 rounded hover:bg-gray-50">
|
|
||||||
<Checkbox value={file.id}>
|
|
||||||
<div className="truncate">{file.name || file.fileName}</div>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
大小: {file.size || 'N/A'} | 创建时间: {file.createdAt ? new Date(file.createdAt).toLocaleString() : 'N/A'}
|
|
||||||
</div>
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Checkbox.Group>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 文件分页 */}
|
|
||||||
{activeDataset && (
|
|
||||||
<div className="p-3 border-t">
|
|
||||||
<Pagination
|
|
||||||
current={filesPage + 1} // 前端UI显示1-based页码
|
|
||||||
total={filesTotal}
|
|
||||||
pageSize={FILES_PAGE_SIZE}
|
|
||||||
onChange={(page) => handleFilesPageChange(page - 1)} // 转换为0-based页码
|
|
||||||
showSizeChanger={false}
|
|
||||||
showQuickJumper
|
|
||||||
showTotal={(total) => `共 ${total} 个文件`}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Form.Item
|
|
||||||
label="分块方式"
|
|
||||||
name="processType"
|
|
||||||
required
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
onChange={(_, value) => setNewKB({ ...newKB, processType: value })}
|
|
||||||
>
|
|
||||||
<Select options={sliceOptions} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<Form.Item
|
|
||||||
label="分块大小"
|
|
||||||
name="chunkSize"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: "请输入分块大小",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onChange={(_, value) => setNewKB({ ...newKB, chunkSize: value })}
|
|
||||||
>
|
|
||||||
<Input type="number" placeholder="请输入分块大小" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="重叠长度"
|
|
||||||
name="overlapSize"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: "请输入重叠长度",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onChange={(_, value) => setNewKB({ ...newKB, overlapSize: value })}
|
|
||||||
>
|
|
||||||
<Input type="number" placeholder="请输入重叠长度" />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && (
|
|
||||||
<Form.Item
|
|
||||||
label="分隔符"
|
|
||||||
name="delimiter"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: "请输入分隔符",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onChange={(_, value) => setNewKB({ ...newKB, delimiter: value })}
|
|
||||||
>
|
|
||||||
<Input placeholder="输入分隔符,如 \n\n" />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="p-4 bg-blue-50 rounded-lg">
|
|
||||||
<div className="text-lg font-medium mb-3">上传信息确认</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="text-gray-600">数据来源:</div>
|
|
||||||
<div className="col-span-2 font-medium">数据集</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="text-gray-600">选择的数据集数:</div>
|
|
||||||
<div className="col-span-2 font-medium">{Object.keys(selectedFilesMap).length}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="text-gray-600">文件总数:</div>
|
|
||||||
<div className="col-span-2 font-medium">{getSelectedFilesCount()}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="text-gray-600">分块方式:</div>
|
|
||||||
<div className="col-span-2 font-medium">
|
|
||||||
{sliceOptions.find(opt => opt.value === newKB.processType)?.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="text-gray-600">分块大小:</div>
|
|
||||||
<div className="col-span-2 font-medium">{newKB.chunkSize}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="text-gray-600">重叠长度:</div>
|
|
||||||
<div className="col-span-2 font-medium">{newKB.overlapSize}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && newKB.delimiter && (
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="text-gray-600">分隔符:</div>
|
|
||||||
<div className="col-span-2 font-medium font-mono">{newKB.delimiter}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 显示每个数据集选择的文件数 */}
|
|
||||||
{Object.keys(selectedFilesMap).length > 0 && (
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-600 mb-2">数据集文件明细:</div>
|
|
||||||
<div className="border rounded-md p-3 bg-white">
|
|
||||||
{datasets.map(dataset => {
|
|
||||||
const selectedCount = selectedFilesMap[dataset.id]?.length || 0;
|
|
||||||
if (selectedCount === 0) return null;
|
|
||||||
return (
|
|
||||||
<div key={dataset.id} className="py-1">
|
|
||||||
<span className="font-medium">{dataset.name}:</span>
|
|
||||||
<span className="text-blue-600">{selectedCount} 个文件</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-yellow-600">
|
|
||||||
提示:上传后系统将自动处理文件,请耐心等待
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
添加数据
|
添加数据
|
||||||
</Button>
|
</Button>
|
||||||
<Modal
|
<Modal
|
||||||
title="添加数据"
|
title="添加数据"
|
||||||
open={isOpen}
|
open={open}
|
||||||
onCancel={handleModalCancel}
|
onCancel={handleModalCancel}
|
||||||
footer={null}
|
footer={
|
||||||
|
<div className="space-x-2">
|
||||||
|
{currentStep > 0 && (
|
||||||
|
<Button disabled={false} onClick={handlePrev}>
|
||||||
|
上一步
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{currentStep < steps.length - 1 ? (
|
||||||
|
<Button type="primary" onClick={handleNext}>
|
||||||
|
下一步
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button type="primary" onClick={handleAddData}>
|
||||||
|
确认上传
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
width={1000}
|
width={1000}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* 步骤导航 */}
|
{/* 步骤导航 */}
|
||||||
<Steps
|
<Steps
|
||||||
current={currentStep}
|
current={currentStep}
|
||||||
onChange={(step) => setCurrentStep(step)}
|
size="small"
|
||||||
className="mb-6"
|
items={steps}
|
||||||
items={steps}
|
labelPlacement="vertical"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 步骤内容 */}
|
{/* 步骤内容 */}
|
||||||
<div className="p-2">
|
<DatasetFileTransfer
|
||||||
<Form
|
hidden={currentStep !== 0}
|
||||||
form={form}
|
open={open}
|
||||||
layout="vertical"
|
selectedMap={selectedMap}
|
||||||
initialValues={newKB}
|
onSelectedChange={setSelectedMap}
|
||||||
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
/>
|
||||||
>
|
|
||||||
{renderStepContent()}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 底部按钮 */}
|
<Form
|
||||||
<div className="flex justify-end gap-4 mt-6">
|
hidden={currentStep !== 1}
|
||||||
{currentStep > 0 && (
|
form={form}
|
||||||
<Button onClick={handlePrev}>
|
layout="vertical"
|
||||||
上一步
|
initialValues={newKB}
|
||||||
</Button>
|
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
||||||
)}
|
>
|
||||||
{currentStep < steps.length - 1 ? (
|
<div className="space-y-6">
|
||||||
<Button type="primary" onClick={handleNext}>
|
<Form.Item
|
||||||
下一步
|
label="分块方式"
|
||||||
</Button>
|
name="processType"
|
||||||
) : (
|
required
|
||||||
<Button type="primary" onClick={handleAddData}>
|
rules={[{ required: true }]}
|
||||||
确认上传
|
>
|
||||||
</Button>
|
<Select options={sliceOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<Form.Item
|
||||||
|
label="分块大小"
|
||||||
|
name="chunkSize"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入分块大小",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input type="number" placeholder="请输入分块大小" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="重叠长度"
|
||||||
|
name="overlapSize"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入重叠长度",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input type="number" placeholder="请输入重叠长度" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && (
|
||||||
|
<Form.Item
|
||||||
|
label="分隔符"
|
||||||
|
name="delimiter"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入分隔符",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="输入分隔符,如 \n\n" />
|
||||||
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<div className="space-y-6" hidden={currentStep !== 2}>
|
||||||
|
<div className="p-4 bg-blue-50 rounded-lg">
|
||||||
|
<div className="text-lg font-medium mb-3">上传信息确认</div>
|
||||||
|
<Descriptions items={descItems} />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-yellow-600">
|
||||||
|
提示:上传后系统将自动处理文件,请耐心等待
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
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;
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Table, Transfer } from "antd";
|
|
||||||
import type {
|
|
||||||
GetProp,
|
|
||||||
TableColumnsType,
|
|
||||||
TableProps,
|
|
||||||
TransferProps,
|
|
||||||
} from "antd";
|
|
||||||
|
|
||||||
type TransferItem = GetProp<TransferProps, "dataSource">[number];
|
|
||||||
type TableRowSelection<T extends object> = TableProps<T>["rowSelection"];
|
|
||||||
|
|
||||||
interface DataType {
|
|
||||||
key: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableTransferProps extends TransferProps<TransferItem> {
|
|
||||||
dataSource: DataType[];
|
|
||||||
leftColumns: TableColumnsType<DataType>;
|
|
||||||
rightColumns: TableColumnsType<DataType>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Customize Table Transfer
|
|
||||||
const TableTransfer: React.FC<TableTransferProps> = (props) => {
|
|
||||||
const { leftColumns, rightColumns, ...restProps } = props;
|
|
||||||
return (
|
|
||||||
<Transfer style={{ width: "100%" }} {...restProps}>
|
|
||||||
{({
|
|
||||||
direction,
|
|
||||||
filteredItems,
|
|
||||||
onItemSelect,
|
|
||||||
onItemSelectAll,
|
|
||||||
selectedKeys: listSelectedKeys,
|
|
||||||
disabled: listDisabled,
|
|
||||||
}) => {
|
|
||||||
const columns = direction === "left" ? leftColumns : rightColumns;
|
|
||||||
const rowSelection: TableRowSelection<TransferItem> = {
|
|
||||||
getCheckboxProps: () => ({ disabled: listDisabled }),
|
|
||||||
onChange(selectedRowKeys) {
|
|
||||||
onItemSelectAll(selectedRowKeys, "replace");
|
|
||||||
},
|
|
||||||
selectedRowKeys: listSelectedKeys,
|
|
||||||
selections: [
|
|
||||||
Table.SELECTION_ALL,
|
|
||||||
Table.SELECTION_INVERT,
|
|
||||||
Table.SELECTION_NONE,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
columns={columns}
|
|
||||||
dataSource={filteredItems}
|
|
||||||
size="small"
|
|
||||||
scroll={{ y: 300 }}
|
|
||||||
style={{ pointerEvents: listDisabled ? "none" : undefined }}
|
|
||||||
onRow={({ key, disabled: itemDisabled }) => ({
|
|
||||||
onClick: () => {
|
|
||||||
if (itemDisabled || listDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onItemSelect(key, !listSelectedKeys.includes(key));
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Transfer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableTransfer;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||||
import {
|
import {
|
||||||
Steps,
|
Steps,
|
||||||
@@ -36,18 +36,21 @@ import {
|
|||||||
Brain,
|
Brain,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link, useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
import { queryDatasetsUsingGet } from "../DataManagement/dataset.api";
|
||||||
|
import { formatBytes } from "@/utils/unit";
|
||||||
|
import DatasetFileTransfer from "../KnowledgeBase/components/DatasetFileTransfer";
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
export default function SynthesisTaskCreate() {
|
export default function SynthesisTaskCreate() {
|
||||||
return <DevelopmentInProgress showTime="2025.11.30" />;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [createStep, setCreateStep] = useState(1);
|
const [createStep, setCreateStep] = useState(1);
|
||||||
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||||||
const [datasets] = useState<Dataset[]>([]);
|
const [selectedMap, setSelectedMap] = useState<Record<string, DatasetFile[]>>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
const [files] = useState<File[]>([]);
|
const [files] = useState<File[]>([]);
|
||||||
const [selectedSynthesisTypes, setSelectedSynthesisTypes] = useState<
|
const [selectedSynthesisTypes, setSelectedSynthesisTypes] = useState<
|
||||||
string[]
|
string[]
|
||||||
@@ -59,6 +62,15 @@ export default function SynthesisTaskCreate() {
|
|||||||
"distillation",
|
"distillation",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fetchDatasets = async () => {
|
||||||
|
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
|
||||||
|
setDatasets(data.content || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDatasets();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const [formValues, setFormValues] = useState({
|
const [formValues, setFormValues] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -270,7 +282,7 @@ export default function SynthesisTaskCreate() {
|
|||||||
const renderCreateTaskPage = () => {
|
const renderCreateTaskPage = () => {
|
||||||
if (createStep === 1) {
|
if (createStep === 1) {
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-y-auto p-2">
|
<div className="flex-1 p-4 overflow-auto">
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
@@ -305,152 +317,11 @@ export default function SynthesisTaskCreate() {
|
|||||||
className="resize-none text-sm"
|
className="resize-none text-sm"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<DatasetFileTransfer
|
||||||
label="源数据集"
|
open
|
||||||
name="sourceDataset"
|
selectedMap={selectedMap}
|
||||||
rules={[{ required: true, message: "请选择数据集" }]}
|
onSelectedChange={setSelectedMap}
|
||||||
>
|
/>
|
||||||
<Select
|
|
||||||
className="w-full"
|
|
||||||
placeholder="选择数据集"
|
|
||||||
options={datasets.map((dataset) => ({
|
|
||||||
label: (
|
|
||||||
<div key={dataset.id}>
|
|
||||||
<div className="flex flex-col py-1">
|
|
||||||
<span className="font-medium text-sm">
|
|
||||||
{dataset.name}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500">
|
|
||||||
{dataset.type} • {dataset.total}条 • {dataset.size}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: dataset.id,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
{form.getFieldValue("sourceDataset") && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<span className="text-sm font-semibold text-gray-700">
|
|
||||||
选择文件
|
|
||||||
</span>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{/* 文件选择区域 */}
|
|
||||||
<Card className="border-gray-200">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Search className="w-3 h-3 absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
placeholder="搜索文件..."
|
|
||||||
className="pl-7 h-8 text-sm"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleSelectAllFiles}
|
|
||||||
className="ml-2 text-xs"
|
|
||||||
type="default"
|
|
||||||
>
|
|
||||||
{selectedFiles.length ===
|
|
||||||
files.filter((file) =>
|
|
||||||
file.name
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchQuery.toLowerCase())
|
|
||||||
).length
|
|
||||||
? "取消全选"
|
|
||||||
: "全选"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{files
|
|
||||||
.filter((file) =>
|
|
||||||
file.name
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchQuery.toLowerCase())
|
|
||||||
)
|
|
||||||
.map((file) => (
|
|
||||||
<div
|
|
||||||
key={file.id}
|
|
||||||
className="flex items-center space-x-2 p-2 hover:bg-gray-50 rounded"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedFiles.includes(file.id)}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedFiles([
|
|
||||||
...selectedFiles,
|
|
||||||
file.id,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setSelectedFiles(
|
|
||||||
selectedFiles.filter(
|
|
||||||
(id) => id !== file.id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-gray-900 truncate">
|
|
||||||
{file.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
{file.size} • {file.type}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
{/* 已选文件列表 */}
|
|
||||||
<Card className="border-gray-200">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium">已选文件</span>
|
|
||||||
<Badge count={selectedFiles.length} className="text-xs" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{selectedFiles.length === 0 ? (
|
|
||||||
<div className="text-center py-4 text-xs text-gray-500">
|
|
||||||
暂未选择文件
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
selectedFiles.map((fileId) => {
|
|
||||||
const file = files.find((f) => f.id === fileId);
|
|
||||||
if (!file) return null;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={fileId}
|
|
||||||
className="flex items-center justify-between p-2 bg-blue-50 rounded border border-blue-200"
|
|
||||||
>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-blue-900 truncate">
|
|
||||||
{file.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-blue-600">
|
|
||||||
{file.size} • {file.type}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
onClick={() => handleRemoveSelectedFile(fileId)}
|
|
||||||
className="p-1 h-6 w-6 hover:bg-blue-100"
|
|
||||||
>
|
|
||||||
<X className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<h2 className="font-medium text-gray-900 text-lg mt-6 mb-2">
|
<h2 className="font-medium text-gray-900 text-lg mt-6 mb-2">
|
||||||
任务配置
|
任务配置
|
||||||
</h2>
|
</h2>
|
||||||
@@ -514,32 +385,8 @@ export default function SynthesisTaskCreate() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
|
||||||
<div className="flex gap-2 justify-end">
|
|
||||||
<Button onClick={() => navigate("/data/synthesis/task")}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
form
|
|
||||||
.validateFields()
|
|
||||||
.then(() => setCreateStep(2))
|
|
||||||
.catch(() => {});
|
|
||||||
}}
|
|
||||||
disabled={
|
|
||||||
!form.getFieldValue("name") ||
|
|
||||||
!form.getFieldValue("sourceDataset") ||
|
|
||||||
selectedFiles.length === 0 ||
|
|
||||||
!form.getFieldValue("targetCount")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
下一步
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1224,26 +1071,47 @@ export default function SynthesisTaskCreate() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="h-full flex flex-col">
|
||||||
<div className="p-6">
|
{/* Header */}
|
||||||
{/* Header */}
|
<div className="flex justify-between items-center mb-2">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<Link to="/data/synthesis/task">
|
||||||
<Link to="/data/synthesis/task">
|
<Button type="text">
|
||||||
<Button type="text">
|
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
<h1 className="text-xl font-bold bg-clip-text">创建合成任务</h1>
|
||||||
<h1 className="text-xl font-bold bg-clip-text">创建合成任务</h1>
|
|
||||||
</div>
|
|
||||||
<Steps
|
|
||||||
current={createStep - 1}
|
|
||||||
size="small"
|
|
||||||
items={[{ title: "基本信息" }, { title: "算子编排" }]}
|
|
||||||
style={{ width: "50%", marginLeft: "auto" }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Steps
|
||||||
|
current={createStep - 1}
|
||||||
|
size="small"
|
||||||
|
items={[{ title: "基本信息" }, { title: "算子编排" }]}
|
||||||
|
style={{ width: "50%", marginLeft: "auto" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="border-card flex-overflow-auto">
|
||||||
{renderCreateTaskPage()}
|
{renderCreateTaskPage()}
|
||||||
|
<div className="flex gap-2 justify-end p-4 border-top">
|
||||||
|
<Button onClick={() => navigate("/data/synthesis/task")}>取消</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then(() => setCreateStep(2))
|
||||||
|
.catch(() => {});
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
!form.getFieldValue("name") ||
|
||||||
|
!form.getFieldValue("sourceDataset") ||
|
||||||
|
selectedFiles.length === 0 ||
|
||||||
|
!form.getFieldValue("targetCount")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
下一步
|
||||||
|
<ArrowRight className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,12 +12,10 @@ import {
|
|||||||
import { Plus, ArrowLeft, Play, Save, RefreshCw, Code, X } from "lucide-react";
|
import { Plus, ArrowLeft, Play, Save, RefreshCw, Code, X } from "lucide-react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { mockTemplates } from "@/mock/annotation";
|
import { mockTemplates } from "@/mock/annotation";
|
||||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
export default function InstructionTemplateCreate() {
|
export default function InstructionTemplateCreate() {
|
||||||
return <DevelopmentInProgress showTime="2025.11.30" />;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||||
null
|
null
|
||||||
@@ -129,7 +127,7 @@ export default function InstructionTemplateCreate() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="h-full flex flex-col gap-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -141,171 +139,170 @@ export default function InstructionTemplateCreate() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card className="overflow-y-auto p-2">
|
<div className="flex-overflow-auto border-card p-4">
|
||||||
<Form
|
<div className="flex-1 overflow-auto">
|
||||||
form={form}
|
<Form
|
||||||
layout="vertical"
|
form={form}
|
||||||
initialValues={initialValues}
|
layout="vertical"
|
||||||
autoComplete="off"
|
initialValues={initialValues}
|
||||||
>
|
autoComplete="off"
|
||||||
<h2 className="font-medium text-gray-900 text-lg mb-2">基本信息</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<Form.Item
|
|
||||||
label="模板名称"
|
|
||||||
name="name"
|
|
||||||
rules={[{ required: true, message: "请输入模板名称" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="输入模板名称" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="分类"
|
|
||||||
name="category"
|
|
||||||
rules={[{ required: true, message: "请选择分类" }]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder="选择分类"
|
|
||||||
options={[
|
|
||||||
{ label: "问答对生成", value: "问答对生成" },
|
|
||||||
{ label: "蒸馏数据集", value: "蒸馏数据集" },
|
|
||||||
{ label: "文本生成", value: "文本生成" },
|
|
||||||
{ label: "多模态生成", value: "多模态生成" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
<Form.Item label="模板描述" name="description">
|
|
||||||
<Input placeholder="简要描述模板的用途和特点" />
|
|
||||||
</Form.Item>
|
|
||||||
<h2 className="font-medium text-gray-900 text-lg mt-6 mb-2">
|
|
||||||
Prompt内容
|
|
||||||
</h2>
|
|
||||||
<Form.Item
|
|
||||||
label="Prompt内容"
|
|
||||||
name="prompt"
|
|
||||||
rules={[{ required: true, message: "请输入Prompt内容" }]}
|
|
||||||
>
|
>
|
||||||
<TextArea
|
<h2 className="font-medium text-gray-900 text-lg mb-2">基本信息</h2>
|
||||||
placeholder="输入prompt内容,使用 {变量名} 格式定义变量"
|
<div className="grid grid-cols-2 gap-4">
|
||||||
rows={10}
|
<Form.Item
|
||||||
className="font-mono text-xs resize-none"
|
label="模板名称"
|
||||||
onChange={handlePromptChange}
|
name="name"
|
||||||
/>
|
rules={[{ required: true, message: "请输入模板名称" }]}
|
||||||
</Form.Item>
|
|
||||||
<p className="text-xs text-gray-500 mb-2">
|
|
||||||
提示:使用 {"{变量名}"} 格式定义变量,例如 {"{text}"} 或 {"{input}"}
|
|
||||||
</p>
|
|
||||||
<div className="mb-4">
|
|
||||||
<span className="text-sm font-semibold text-gray-700">
|
|
||||||
变量管理
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-wrap gap-2 min-h-[50px] p-3 border rounded-xl bg-gray-50 mt-2">
|
|
||||||
{variables.map((variable, index) => (
|
|
||||||
<Badge
|
|
||||||
key={index}
|
|
||||||
count={
|
|
||||||
<X
|
|
||||||
className="w-3 h-3 cursor-pointer"
|
|
||||||
onClick={() => handleRemoveVariable(index)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
style={{ backgroundColor: "#fff" }}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-1 px-2 py-1 text-xs">
|
|
||||||
<Code className="w-3 h-3" />
|
|
||||||
{variable}
|
|
||||||
</span>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{variables.length === 0 && (
|
|
||||||
<span className="text-xs text-gray-400">
|
|
||||||
暂无变量,在Prompt中使用 {"{变量名}"} 格式定义
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 mt-2">
|
|
||||||
<Input
|
|
||||||
ref={variableInputRef}
|
|
||||||
placeholder="添加变量名(如:text, input, question)"
|
|
||||||
className="h-8 text-sm"
|
|
||||||
onPressEnter={handleAddVariable}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={handleAddVariable}
|
|
||||||
type="default"
|
|
||||||
className="px-4 text-sm"
|
|
||||||
>
|
>
|
||||||
<Plus className="w-3 h-3 mr-1" />
|
<Input placeholder="输入模板名称" />
|
||||||
添加
|
</Form.Item>
|
||||||
</Button>
|
<Form.Item
|
||||||
|
label="分类"
|
||||||
|
name="category"
|
||||||
|
rules={[{ required: true, message: "请选择分类" }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="选择分类"
|
||||||
|
options={[
|
||||||
|
{ label: "问答对生成", value: "问答对生成" },
|
||||||
|
{ label: "蒸馏数据集", value: "蒸馏数据集" },
|
||||||
|
{ label: "文本生成", value: "文本生成" },
|
||||||
|
{ label: "多模态生成", value: "多模态生成" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Form.Item label="模板描述" name="description">
|
||||||
<h2 className="font-medium text-gray-900 text-lg mb-2 pt-2">
|
<Input placeholder="简要描述模板的用途和特点" />
|
||||||
模板测试
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<Form.Item label="测试输入" name="testInput">
|
|
||||||
<TextArea
|
|
||||||
placeholder="输入测试数据"
|
|
||||||
rows={5}
|
|
||||||
className="resize-none text-sm"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="测试输出" name="testOutput">
|
<h2 className="font-medium text-gray-900 text-lg mt-6 mb-2">
|
||||||
<TextArea
|
Prompt内容
|
||||||
readOnly
|
</h2>
|
||||||
placeholder="点击测试按钮查看输出结果"
|
<Form.Item
|
||||||
rows={5}
|
label="Prompt内容"
|
||||||
className="resize-none bg-gray-50 text-sm"
|
name="prompt"
|
||||||
/>
|
rules={[{ required: true, message: "请输入Prompt内容" }]}
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleTestTemplate}
|
|
||||||
disabled={
|
|
||||||
!form.getFieldValue("prompt") ||
|
|
||||||
!form.getFieldValue("testInput") ||
|
|
||||||
isTestingTemplate
|
|
||||||
}
|
|
||||||
type="default"
|
|
||||||
className="px-4 py-2 text-sm"
|
|
||||||
>
|
|
||||||
{isTestingTemplate ? (
|
|
||||||
<>
|
|
||||||
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
|
|
||||||
测试中...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Play className="w-3 h-3 mr-1" />
|
|
||||||
测试模板
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Divider />
|
|
||||||
<div className="flex gap-2 justify-end">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={handleSaveTemplate}
|
|
||||||
disabled={
|
|
||||||
!form.getFieldValue("name") ||
|
|
||||||
!form.getFieldValue("prompt") ||
|
|
||||||
!form.getFieldValue("category")
|
|
||||||
}
|
|
||||||
className="px-6 py-2 text-sm font-semibold bg-purple-600 hover:bg-purple-700 shadow-lg"
|
|
||||||
>
|
>
|
||||||
<Save className="w-3 h-3 mr-1" />
|
<TextArea
|
||||||
保存模板
|
placeholder="输入prompt内容,使用 {变量名} 格式定义变量"
|
||||||
</Button>
|
rows={10}
|
||||||
|
className="font-mono text-xs resize-none"
|
||||||
|
onChange={handlePromptChange}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<p className="text-xs text-gray-500 mb-2">
|
||||||
|
提示:使用 {"{变量名}"} 格式定义变量,例如 {"{text}"} 或{" "}
|
||||||
|
{"{input}"}
|
||||||
|
</p>
|
||||||
|
<div className="mb-4">
|
||||||
|
<span className="text-sm font-semibold text-gray-700">
|
||||||
|
变量管理
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap gap-2 min-h-[50px] p-3 border rounded-xl bg-gray-50 mt-2">
|
||||||
|
{variables.map((variable, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
count={
|
||||||
|
<X
|
||||||
|
className="w-3 h-3 cursor-pointer"
|
||||||
|
onClick={() => handleRemoveVariable(index)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
style={{ backgroundColor: "#fff" }}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1 px-2 py-1 text-xs">
|
||||||
|
<Code className="w-3 h-3" />
|
||||||
|
{variable}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{variables.length === 0 && (
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
暂无变量,在Prompt中使用 {"{变量名}"} 格式定义
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<Input
|
||||||
|
ref={variableInputRef}
|
||||||
|
placeholder="添加变量名(如:text, input, question)"
|
||||||
|
className="h-8 text-sm"
|
||||||
|
onPressEnter={handleAddVariable}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddVariable}
|
||||||
|
type="default"
|
||||||
|
className="px-4 text-sm"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3 mr-1" />
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="font-medium text-gray-900 text-lg mb-2 pt-2">
|
||||||
|
模板测试
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Form.Item label="测试输入" name="testInput">
|
||||||
|
<TextArea
|
||||||
|
placeholder="输入测试数据"
|
||||||
|
rows={5}
|
||||||
|
className="resize-none text-sm"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="测试输出" name="testOutput">
|
||||||
|
<TextArea
|
||||||
|
readOnly
|
||||||
|
placeholder="点击测试按钮查看输出结果"
|
||||||
|
rows={5}
|
||||||
|
className="resize-none bg-gray-50 text-sm"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate("/data/synthesis/task")}
|
onClick={handleTestTemplate}
|
||||||
|
disabled={
|
||||||
|
!form.getFieldValue("prompt") ||
|
||||||
|
!form.getFieldValue("testInput") ||
|
||||||
|
isTestingTemplate
|
||||||
|
}
|
||||||
type="default"
|
type="default"
|
||||||
className="px-4 py-2 text-sm"
|
className="px-4 py-2 text-sm"
|
||||||
>
|
>
|
||||||
取消
|
{isTestingTemplate ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
|
||||||
|
测试中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>测试模板</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Form>
|
||||||
</Form>
|
</div>
|
||||||
</Card>
|
<div className="flex gap-2 justify-end p-4 border-top">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSaveTemplate}
|
||||||
|
disabled={
|
||||||
|
!form.getFieldValue("name") ||
|
||||||
|
!form.getFieldValue("prompt") ||
|
||||||
|
!form.getFieldValue("category")
|
||||||
|
}
|
||||||
|
className="px-6 py-2 text-sm font-semibold bg-purple-600 hover:bg-purple-700 shadow-lg"
|
||||||
|
>
|
||||||
|
<Save className="w-3 h-3 mr-1" />
|
||||||
|
保存模板
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate("/data/synthesis/task")}
|
||||||
|
type="default"
|
||||||
|
className="px-4 py-2 text-sm"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Tabs, Button } from "antd";
|
import { Tabs, Button } from "antd";
|
||||||
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { Plus, ArrowRight } from "lucide-react";
|
import { Plus, ArrowRight } from "lucide-react";
|
||||||
import DataAnnotation from "../DataAnnotation/Annotate/components/TextAnnotation";
|
import DataAnnotation from "../DataAnnotation/Annotate/components/TextAnnotation";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import InstructionTemplateTab from "./components/InstructionTemplateTab";
|
import InstructionTemplateTab from "./components/InstructionTemplateTab";
|
||||||
import SynthesisTaskTab from "./components/SynthesisTaskTab";
|
import SynthesisTaskTab from "./components/SynthesisTaskTab";
|
||||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
|
||||||
|
|
||||||
export default function DataSynthesisPage() {
|
export default function DataSynthesisPage() {
|
||||||
return <DevelopmentInProgress showTime="2025.11.30" />;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState("tasks");
|
const [activeTab, setActiveTab] = useState("tasks");
|
||||||
@@ -40,45 +39,42 @@ export default function DataSynthesisPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="h-full flex flex-col">
|
||||||
<div className=" p-6">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-1">
|
||||||
<div className="space-y-1">
|
<h2 className="text-xl font-bold text-gray-900">数据合成</h2>
|
||||||
<h2 className="text-xl font-bold text-gray-900">数据合成</h2>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
<div className="flex items-center gap-2">
|
onClick={() => {
|
||||||
<Button
|
navigate("/data/synthesis/task/create-template");
|
||||||
onClick={() => {
|
}}
|
||||||
navigate("/data/synthesis/task/create-template");
|
icon={<PlusOutlined />}
|
||||||
}}
|
>
|
||||||
>
|
创建模板
|
||||||
<Plus className="w-3 h-3 mr-1" />
|
</Button>
|
||||||
创建模板
|
<Button
|
||||||
</Button>
|
type="primary"
|
||||||
<Button
|
onClick={() => navigate("/data/synthesis/task/create")}
|
||||||
type="primary"
|
icon={<PlusOutlined />}
|
||||||
onClick={() => navigate("/data/synthesis/task/create")}
|
>
|
||||||
>
|
创建合成任务
|
||||||
<Plus className="w-3 h-3 mr-1" />
|
</Button>
|
||||||
创建合成任务
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
|
||||||
items={[
|
|
||||||
{ key: "tasks", label: "合成任务", children: <SynthesisTaskTab /> },
|
|
||||||
{
|
|
||||||
key: "templates",
|
|
||||||
label: "指令模板",
|
|
||||||
children: <InstructionTemplateTab />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
activeKey={activeTab}
|
|
||||||
onChange={setActiveTab}
|
|
||||||
></Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
items={[
|
||||||
|
{ key: "tasks", label: "合成任务", children: <SynthesisTaskTab /> },
|
||||||
|
{
|
||||||
|
key: "templates",
|
||||||
|
label: "指令模板",
|
||||||
|
children: <InstructionTemplateTab />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={setActiveTab}
|
||||||
|
></Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Card, Table, Badge, Button } from "antd";
|
import { Card, Table, Badge, Button } from "antd";
|
||||||
import {
|
import { EditOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
Plus,
|
import { Plus, FileText } from "lucide-react";
|
||||||
FileText,
|
|
||||||
Search,
|
|
||||||
Edit,
|
|
||||||
Copy,
|
|
||||||
Trash2,
|
|
||||||
MoreHorizontal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { Template } from "@/pages/SynthesisTask/synthesis";
|
import type { Template } from "@/pages/SynthesisTask/synthesis";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { mockTemplates } from "@/mock/synthesis";
|
import { mockTemplates } from "@/mock/synthesis";
|
||||||
@@ -45,6 +38,7 @@ export default function InstructionTemplateTab() {
|
|||||||
title: "模板名称",
|
title: "模板名称",
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
key: "name",
|
key: "name",
|
||||||
|
fixed: "left",
|
||||||
render: (text: string, template: Template) => (
|
render: (text: string, template: Template) => (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 bg-purple-500 rounded-lg flex items-center justify-center shadow-sm">
|
<div className="w-8 h-8 bg-purple-500 rounded-lg flex items-center justify-center shadow-sm">
|
||||||
@@ -103,14 +97,7 @@ export default function InstructionTemplateTab() {
|
|||||||
title: "质量评分",
|
title: "质量评分",
|
||||||
dataIndex: "quality",
|
dataIndex: "quality",
|
||||||
key: "quality",
|
key: "quality",
|
||||||
render: (quality: number) =>
|
render: (quality: number) => (quality ? `${quality}%` : "-"),
|
||||||
quality ? (
|
|
||||||
<Badge className={`font-medium text-xs ${getQualityColor(quality)}`}>
|
|
||||||
{quality}%
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-gray-400">-</span>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "最后使用",
|
title: "最后使用",
|
||||||
@@ -123,25 +110,14 @@ export default function InstructionTemplateTab() {
|
|||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
align: "center" as const,
|
fixed: "right",
|
||||||
render: (_: any, template: Template) => (
|
render: (_: any, template: Template) => (
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center">
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/data/synthesis/task/create-template/${template.id}`)
|
|
||||||
}
|
|
||||||
type="text"
|
|
||||||
>
|
|
||||||
<Edit className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
<Button type="text">
|
<Button type="text">
|
||||||
<Copy className="w-3 h-3" />
|
<EditOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="text">
|
<Button type="text" danger>
|
||||||
<Trash2 className="w-3 h-3" />
|
<DeleteOutlined />
|
||||||
</Button>
|
|
||||||
<Button type="text">
|
|
||||||
<MoreHorizontal className="w-3 h-3" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
Check,
|
||||||
|
StopCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { SynthesisTask } from "@/pages/SynthesisTask/synthesis";
|
import type { SynthesisTask } from "@/pages/SynthesisTask/synthesis";
|
||||||
import { mockSynthesisTasks } from "@/mock/synthesis";
|
import { mockSynthesisTasks } from "@/mock/synthesis";
|
||||||
import { useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import { SearchControls } from "@/components/SearchControls";
|
import { SearchControls } from "@/components/SearchControls";
|
||||||
|
import { formatDateTime } from "@/utils/unit";
|
||||||
|
|
||||||
export default function SynthesisTaskTab() {
|
export default function SynthesisTaskTab() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -73,28 +76,28 @@ export default function SynthesisTaskTab() {
|
|||||||
const getStatusBadge = (status: string) => {
|
const getStatusBadge = (status: string) => {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
pending: {
|
pending: {
|
||||||
span: "等待中",
|
label: "等待中",
|
||||||
color: "bg-yellow-50 text-yellow-700 border-yellow-200",
|
color: "#F59E0B",
|
||||||
icon: Pause,
|
icon: Pause,
|
||||||
},
|
},
|
||||||
running: {
|
running: {
|
||||||
span: "运行中",
|
label: "运行中",
|
||||||
color: "bg-blue-50 text-blue-700 border-blue-200",
|
color: "#3B82F6",
|
||||||
icon: Play,
|
icon: Play,
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
span: "已完成",
|
label: "已完成",
|
||||||
color: "bg-green-50 text-green-700 border-green-200",
|
color: "#10B981",
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
},
|
},
|
||||||
failed: {
|
failed: {
|
||||||
span: "失败",
|
label: "失败",
|
||||||
color: "bg-red-50 text-red-700 border-red-200",
|
color: "#EF4444",
|
||||||
icon: Pause,
|
icon: Pause,
|
||||||
},
|
},
|
||||||
paused: {
|
paused: {
|
||||||
span: "已暂停",
|
label: "已暂停",
|
||||||
color: "bg-gray-50 text-gray-700 border-gray-200",
|
color: "#E5E7EB",
|
||||||
icon: Pause,
|
icon: Pause,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -130,6 +133,7 @@ export default function SynthesisTaskTab() {
|
|||||||
),
|
),
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
key: "name",
|
key: "name",
|
||||||
|
fixed: "left" as const,
|
||||||
render: (text: string, task: SynthesisTask) => (
|
render: (text: string, task: SynthesisTask) => (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center shadow-sm">
|
<div className="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center shadow-sm">
|
||||||
@@ -139,7 +143,7 @@ export default function SynthesisTaskTab() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900 text-sm">{task.name}</div>
|
<Link to={`/data/synthesis/task/${task.id}`}>{task.name}</Link>
|
||||||
<div className="text-xs text-gray-500">{task.template}</div>
|
<div className="text-xs text-gray-500">{task.template}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,11 +153,7 @@ export default function SynthesisTaskTab() {
|
|||||||
title: "类型",
|
title: "类型",
|
||||||
dataIndex: "type",
|
dataIndex: "type",
|
||||||
key: "type",
|
key: "type",
|
||||||
render: (type: string) => (
|
render: (type: string) => type.toUpperCase(),
|
||||||
<Badge className="bg-blue-50 text-blue-700 border-blue-200 text-xs">
|
|
||||||
{type?.toUpperCase()}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "状态",
|
title: "状态",
|
||||||
@@ -161,38 +161,17 @@ export default function SynthesisTaskTab() {
|
|||||||
key: "status",
|
key: "status",
|
||||||
render: (status: string) => {
|
render: (status: string) => {
|
||||||
const statusConfig = getStatusBadge(status);
|
const statusConfig = getStatusBadge(status);
|
||||||
const StatusIcon = statusConfig.icon;
|
return <Badge color={statusConfig.color} text={statusConfig.label} />;
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
className={`${statusConfig.color} flex items-center gap-1 w-fit text-xs`}
|
|
||||||
>
|
|
||||||
<StatusIcon className="w-3 h-3" />
|
|
||||||
{statusConfig.span}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "进度",
|
title: "进度",
|
||||||
dataIndex: "progress",
|
dataIndex: "progress",
|
||||||
key: "progress",
|
key: "progress",
|
||||||
render: (_: any, task: SynthesisTask) =>
|
width: 150,
|
||||||
task.status === "running" ? (
|
render: (_: any, task: SynthesisTask) => (
|
||||||
<div className="space-y-1">
|
<Progress percent={task.progress} size="small" />
|
||||||
<Progress percent={task.progress} size="small" showInfo={false} />
|
),
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{Math.round(task.progress)}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
{task.status === "completed"
|
|
||||||
? "100%"
|
|
||||||
: task.status === "failed"
|
|
||||||
? `${Math.round(task.progress)}%`
|
|
||||||
: "-"}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "源数据集",
|
title: "源数据集",
|
||||||
@@ -217,48 +196,18 @@ export default function SynthesisTaskTab() {
|
|||||||
title: "质量评分",
|
title: "质量评分",
|
||||||
dataIndex: "quality",
|
dataIndex: "quality",
|
||||||
key: "quality",
|
key: "quality",
|
||||||
render: (quality: number) =>
|
render: (quality: number) => (quality ? `${quality}%` : "-"),
|
||||||
quality ? (
|
|
||||||
<Badge className="font-medium text-xs text-green-600 bg-green-50 border-green-200">
|
|
||||||
{quality}%
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-gray-400">-</span>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: (
|
title: "创建时间",
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
onClick={() => {
|
|
||||||
if (sortBy === "createdAt") {
|
|
||||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
|
||||||
} else {
|
|
||||||
setSortBy("createdAt");
|
|
||||||
setSortOrder("desc");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="h-auto p-0 font-semibold text-gray-700 hover:bg-transparent"
|
|
||||||
>
|
|
||||||
创建时间
|
|
||||||
{sortBy === "createdAt" &&
|
|
||||||
(sortOrder === "asc" ? (
|
|
||||||
<ArrowUp className="w-3 h-3 ml-1" />
|
|
||||||
) : (
|
|
||||||
<ArrowDown className="w-3 h-3 ml-1" />
|
|
||||||
))}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
dataIndex: "createdAt",
|
dataIndex: "createdAt",
|
||||||
key: "createdAt",
|
key: "createdAt",
|
||||||
render: (createdAt: string) => (
|
render: formatDateTime,
|
||||||
<div className="text-sm text-gray-600">{createdAt}</div>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
align: "center" as const,
|
fixed: "right" as const,
|
||||||
render: (_: any, task: SynthesisTask) => (
|
render: (_: any, task: SynthesisTask) => (
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
{task.status === "running" && (
|
{task.status === "running" && (
|
||||||
@@ -266,36 +215,24 @@ export default function SynthesisTaskTab() {
|
|||||||
onClick={() => handleTaskAction(task.id, "pause")}
|
onClick={() => handleTaskAction(task.id, "pause")}
|
||||||
className="hover:bg-orange-50 p-1 h-7 w-7"
|
className="hover:bg-orange-50 p-1 h-7 w-7"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
icon={<Pause className="w-4 h-4" />}
|
||||||
<Pause className="w-3 h-3" />
|
></Button>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
{task.status === "paused" && (
|
{task.status === "paused" && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleTaskAction(task.id, "resume")}
|
onClick={() => handleTaskAction(task.id, "resume")}
|
||||||
className="hover:bg-green-50 p-1 h-7 w-7"
|
className="hover:bg-green-50 p-1 h-7 w-7"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
icon={<Play className="w-4 h-4" />}
|
||||||
<Play className="w-3 h-3" />
|
></Button>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
className="hover:bg-blue-50 p-2 h-7 w-7"
|
|
||||||
type="text"
|
|
||||||
onClick={() => navigate(`/data/synthesis/task/${task.id}`)}
|
|
||||||
>
|
|
||||||
审核
|
|
||||||
</Button>
|
|
||||||
<Button className="hover:bg-green-50 p-1 h-7 w-7" type="text">
|
|
||||||
<DownloadIcon className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="space-y-4">
|
||||||
{/* 搜索和筛选 */}
|
{/* 搜索和筛选 */}
|
||||||
<SearchControls
|
<SearchControls
|
||||||
searchTerm={searchQuery}
|
searchTerm={searchQuery}
|
||||||
|
|||||||
Reference in New Issue
Block a user