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 {
|
||||
Button,
|
||||
App,
|
||||
@@ -7,31 +7,13 @@ import {
|
||||
Form,
|
||||
Modal,
|
||||
Steps,
|
||||
Empty,
|
||||
Checkbox,
|
||||
Pagination,
|
||||
Space,
|
||||
Descriptions,
|
||||
} from "antd";
|
||||
import { SearchOutlined, 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 { PlusOutlined } from "@ant-design/icons";
|
||||
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
||||
import { DatasetType } from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
// 定义简单的FileInfo接口,确保与API兼容
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
name?: string;
|
||||
fileName?: string;
|
||||
size?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
const { Step } = Steps;
|
||||
import DatasetFileTransfer from "./DatasetFileTransfer";
|
||||
import { DescriptionsItemType } from "antd/es/descriptions";
|
||||
import { DatasetFile } from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
const sliceOptions = [
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
@@ -42,34 +24,22 @@ const sliceOptions = [
|
||||
];
|
||||
|
||||
export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
// 数据集相关状态
|
||||
const [datasets, setDatasets] = useState<any[]>([]);
|
||||
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 [selectedMap, setSelectedMap] = useState<Record<string, DatasetFile[]>>(
|
||||
{}
|
||||
);
|
||||
|
||||
// 定义分块选项
|
||||
const sliceOptions = [
|
||||
{ label: '默认分块', value: 'DEFAULT_CHUNK' },
|
||||
{ label: '按章节分块', value: 'CHAPTER_CHUNK' },
|
||||
{ label: '按段落分块', value: 'PARAGRAPH_CHUNK' },
|
||||
{ label: '固定长度分块', value: 'FIXED_LENGTH_CHUNK' },
|
||||
{ label: '自定义分隔符分块', value: 'CUSTOM_SEPARATOR_CHUNK' },
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
{ label: "按章节分块", value: "CHAPTER_CHUNK" },
|
||||
{ label: "按段落分块", value: "PARAGRAPH_CHUNK" },
|
||||
{ label: "固定长度分块", value: "FIXED_LENGTH_CHUNK" },
|
||||
{ label: "自定义分隔符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||
];
|
||||
|
||||
// 定义初始状态
|
||||
@@ -77,224 +47,56 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: '',
|
||||
delimiter: "",
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: '选择数据集文件',
|
||||
description: '从多个数据集中选择文件',
|
||||
title: "选择数据集文件",
|
||||
description: "从多个数据集中选择文件",
|
||||
},
|
||||
{
|
||||
title: '配置参数',
|
||||
description: '设置数据处理参数',
|
||||
title: "配置参数",
|
||||
description: "设置数据处理参数",
|
||||
},
|
||||
{
|
||||
title: '确认上传',
|
||||
description: '确认信息并上传',
|
||||
title: "确认上传",
|
||||
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 = () => {
|
||||
return Object.values(selectedFilesMap).reduce((total, ids) => total + ids.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;
|
||||
return Object.values(selectedMap).reduce(
|
||||
(total, files) => total + files.length,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
// 验证当前步骤
|
||||
if (currentStep === 0) {
|
||||
if (getSelectedFilesCount() === 0) {
|
||||
message.warning('请至少选择一个文件');
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currentStep === 1) {
|
||||
// 验证切片参数
|
||||
if (!newKB.processType) {
|
||||
message.warning('请选择分块方式');
|
||||
message.warning("请选择分块方式");
|
||||
return;
|
||||
}
|
||||
if (!newKB.chunkSize || Number(newKB.chunkSize) <= 0) {
|
||||
message.warning('请输入有效的分块大小');
|
||||
message.warning("请输入有效的分块大小");
|
||||
return;
|
||||
}
|
||||
if (!newKB.overlapSize || Number(newKB.overlapSize) < 0) {
|
||||
message.warning('请输入有效的重叠长度');
|
||||
message.warning("请输入有效的重叠长度");
|
||||
return;
|
||||
}
|
||||
if (newKB.processType === 'CUSTOM_SEPARATOR_CHUNK' && !newKB.delimiter) {
|
||||
message.warning('请输入分隔符');
|
||||
if (newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && !newKB.delimiter) {
|
||||
message.warning("请输入分隔符");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -308,93 +110,38 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||
// 重置所有状态
|
||||
const handleReset = () => {
|
||||
setCurrentStep(0);
|
||||
setSelectedFilesMap({});
|
||||
setNewKB({
|
||||
processType: 'DEFAULT_CHUNK',
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: '',
|
||||
delimiter: "",
|
||||
});
|
||||
setDatasets([]);
|
||||
setDatasetPage(1);
|
||||
setDatasetSearch('');
|
||||
setDatasetFiles([]);
|
||||
setFilesPage(1);
|
||||
setFileSearch('');
|
||||
setActiveDataset(null);
|
||||
setLoadedFilesCache({}); // 清除文件缓存
|
||||
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 selectedFiles = [];
|
||||
const files = [];
|
||||
|
||||
Object.entries(selectedFilesMap).forEach(([datasetId, fileIds]) => {
|
||||
fileIds.forEach(fileId => {
|
||||
// 查找文件信息以获取文件名
|
||||
const fileInfo = datasetFiles.find(file => file.id === fileId);
|
||||
// 根据API定义,需要id和name字段
|
||||
selectedFiles.push({
|
||||
id: fileId,
|
||||
name: fileInfo?.name || fileInfo?.fileName || `文件_${fileId}`
|
||||
});
|
||||
});
|
||||
});
|
||||
Object.entries(selectedMap).forEach(([datasetId, fileList]) => {
|
||||
files.push(
|
||||
...fileList.map((file) => ({
|
||||
...file,
|
||||
id: file.id,
|
||||
name: file.fileName,
|
||||
datasetId,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning('请至少选择一个文件');
|
||||
if (files.length === 0) {
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造符合API要求的请求数据
|
||||
const requestData = {
|
||||
files: selectedFiles,
|
||||
files,
|
||||
processType: newKB.processType,
|
||||
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
||||
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
||||
@@ -406,396 +153,188 @@ export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||
// 先通知父组件刷新数据(确保刷新发生在重置前)
|
||||
onDataAdded?.();
|
||||
|
||||
message.success('数据添加成功');
|
||||
message.success("数据添加成功");
|
||||
// 重置状态
|
||||
handleReset();
|
||||
setIsOpen(false);
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
message.error('数据添加失败,请重试');
|
||||
console.error('添加文件失败:', error);
|
||||
message.error("数据添加失败,请重试");
|
||||
console.error("添加文件失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// handleReset函数已在前面定义,删除重复定义
|
||||
|
||||
const handleModalCancel = () => {
|
||||
handleReset();
|
||||
setIsOpen(false);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 0:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-lg font-medium">选择数据集文件</div>
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
请从左侧选择数据集,然后在右侧选择需要导入的文件。支持从多个不同数据集中交叉选择文件。
|
||||
</div>
|
||||
|
||||
{getSelectedFilesCount() > 0 && (
|
||||
<div className="p-3 bg-blue-50 rounded-md text-blue-700">
|
||||
已选择 {getSelectedFilesCount()} 个文件(来自 {Object.keys(selectedFilesMap).length} 个数据集)
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 h-[500px]">
|
||||
{/* 左侧数据集列表(带搜索和分页) */}
|
||||
<div className="border rounded-lg flex flex-col h-full">
|
||||
<div className="p-4 border-b">
|
||||
<div className="font-medium mb-3">数据集列表</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="搜索数据集名称"
|
||||
value={datasetSearch}
|
||||
onChange={(e) => setDatasetSearch(e.target.value)}
|
||||
onPressEnter={handleDatasetSearch}
|
||||
prefix={<SearchOutlined />}
|
||||
suffix={
|
||||
<Button type="primary" size="small" onClick={handleDatasetSearch}>
|
||||
搜索
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 max-h-[400px]">
|
||||
{datasetsLoading ? (
|
||||
<div className="flex justify-center items-center h-full">加载中...</div>
|
||||
) : datasets.length === 0 ? (
|
||||
<Empty description="暂无可用数据集" />
|
||||
) : (
|
||||
<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;
|
||||
}
|
||||
};
|
||||
const descItems: DescriptionsItemType[] = [
|
||||
{
|
||||
label: "知识库名称",
|
||||
key: "knowledgeBaseName",
|
||||
children: knowledgeBase?.name,
|
||||
},
|
||||
{
|
||||
label: "数据来源",
|
||||
key: "dataSource",
|
||||
children: "数据集",
|
||||
},
|
||||
{
|
||||
label: "选择的数据集数",
|
||||
key: "selectedDatasetCount",
|
||||
children: Object.keys(selectedMap).length,
|
||||
},
|
||||
{
|
||||
label: "文件总数",
|
||||
key: "totalFileCount",
|
||||
children: getSelectedFilesCount(),
|
||||
},
|
||||
{
|
||||
label: "分块方式",
|
||||
key: "chunkingMethod",
|
||||
children:
|
||||
sliceOptions.find((opt) => opt.value === newKB.processType)?.label ||
|
||||
"",
|
||||
},
|
||||
{
|
||||
label: "分块大小",
|
||||
key: "chunkSize",
|
||||
children: newKB.chunkSize,
|
||||
},
|
||||
{
|
||||
label: "重叠长度",
|
||||
key: "overlapSize",
|
||||
children: newKB.overlapSize,
|
||||
},
|
||||
...(newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && newKB.delimiter
|
||||
? [
|
||||
{
|
||||
label: "分隔符",
|
||||
children: <span className="font-mono">{newKB.delimiter}</span>,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsOpen(true)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
添加数据
|
||||
</Button>
|
||||
<Modal
|
||||
title="添加数据"
|
||||
open={isOpen}
|
||||
open={open}
|
||||
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}
|
||||
>
|
||||
<div>
|
||||
{/* 步骤导航 */}
|
||||
<Steps
|
||||
current={currentStep}
|
||||
onChange={(step) => setCurrentStep(step)}
|
||||
className="mb-6"
|
||||
items={steps}
|
||||
/>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
size="small"
|
||||
items={steps}
|
||||
labelPlacement="vertical"
|
||||
/>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
<div className="p-2">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={newKB}
|
||||
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
||||
>
|
||||
{renderStepContent()}
|
||||
</Form>
|
||||
</div>
|
||||
<DatasetFileTransfer
|
||||
hidden={currentStep !== 0}
|
||||
open={open}
|
||||
selectedMap={selectedMap}
|
||||
onSelectedChange={setSelectedMap}
|
||||
/>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className="flex justify-end gap-4 mt-6">
|
||||
{currentStep > 0 && (
|
||||
<Button onClick={handlePrev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < steps.length - 1 ? (
|
||||
<Button type="primary" onClick={handleNext}>
|
||||
下一步
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" onClick={handleAddData}>
|
||||
确认上传
|
||||
</Button>
|
||||
<Form
|
||||
hidden={currentStep !== 1}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={newKB}
|
||||
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<Form.Item
|
||||
label="分块方式"
|
||||
name="processType"
|
||||
required
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<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>
|
||||
</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 {
|
||||
Steps,
|
||||
@@ -36,18 +36,21 @@ import {
|
||||
Brain,
|
||||
} from "lucide-react";
|
||||
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;
|
||||
|
||||
export default function SynthesisTaskCreate() {
|
||||
return <DevelopmentInProgress showTime="2025.11.30" />;
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [createStep, setCreateStep] = useState(1);
|
||||
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||||
const [datasets] = useState<Dataset[]>([]);
|
||||
const [selectedMap, setSelectedMap] = useState<Record<string, DatasetFile[]>>(
|
||||
{}
|
||||
);
|
||||
const [files] = useState<File[]>([]);
|
||||
const [selectedSynthesisTypes, setSelectedSynthesisTypes] = useState<
|
||||
string[]
|
||||
@@ -59,6 +62,15 @@ export default function SynthesisTaskCreate() {
|
||||
"distillation",
|
||||
]);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
|
||||
setDatasets(data.content || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDatasets();
|
||||
}, []);
|
||||
|
||||
// 表单数据
|
||||
const [formValues, setFormValues] = useState({
|
||||
name: "",
|
||||
@@ -270,7 +282,7 @@ export default function SynthesisTaskCreate() {
|
||||
const renderCreateTaskPage = () => {
|
||||
if (createStep === 1) {
|
||||
return (
|
||||
<Card className="overflow-y-auto p-2">
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@@ -305,152 +317,11 @@ export default function SynthesisTaskCreate() {
|
||||
className="resize-none text-sm"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="源数据集"
|
||||
name="sourceDataset"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
|
||||
<DatasetFileTransfer
|
||||
open
|
||||
selectedMap={selectedMap}
|
||||
onSelectedChange={setSelectedMap}
|
||||
/>
|
||||
<h2 className="font-medium text-gray-900 text-lg mt-6 mb-2">
|
||||
任务配置
|
||||
</h2>
|
||||
@@ -514,32 +385,8 @@ export default function SynthesisTaskCreate() {
|
||||
</Form.Item>
|
||||
</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>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1224,26 +1071,47 @@ export default function SynthesisTaskCreate() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="p-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/synthesis/task">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<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 className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/synthesis/task">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<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 className="border-card flex-overflow-auto">
|
||||
{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>
|
||||
);
|
||||
|
||||
@@ -12,12 +12,10 @@ import {
|
||||
import { Plus, ArrowLeft, Play, Save, RefreshCw, Code, X } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { mockTemplates } from "@/mock/annotation";
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
export default function InstructionTemplateCreate() {
|
||||
return <DevelopmentInProgress showTime="2025.11.30" />;
|
||||
const navigate = useNavigate();
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||
null
|
||||
@@ -129,7 +127,7 @@ export default function InstructionTemplateCreate() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center">
|
||||
@@ -141,171 +139,170 @@ export default function InstructionTemplateCreate() {
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<Card className="overflow-y-auto p-2">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
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内容" }]}
|
||||
<div className="flex-overflow-auto border-card p-4">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={initialValues}
|
||||
autoComplete="off"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="输入prompt内容,使用 {变量名} 格式定义变量"
|
||||
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"
|
||||
<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: "请输入模板名称" }]}
|
||||
>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
<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>
|
||||
</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 label="模板描述" name="description">
|
||||
<Input placeholder="简要描述模板的用途和特点" />
|
||||
</Form.Item>
|
||||
<Form.Item label="测试输出" name="testOutput">
|
||||
<TextArea
|
||||
readOnly
|
||||
placeholder="点击测试按钮查看输出结果"
|
||||
rows={5}
|
||||
className="resize-none bg-gray-50 text-sm"
|
||||
/>
|
||||
</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"
|
||||
<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内容" }]}
|
||||
>
|
||||
<Save className="w-3 h-3 mr-1" />
|
||||
保存模板
|
||||
</Button>
|
||||
<TextArea
|
||||
placeholder="输入prompt内容,使用 {变量名} 格式定义变量"
|
||||
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
|
||||
onClick={() => navigate("/data/synthesis/task")}
|
||||
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" />
|
||||
测试中...
|
||||
</>
|
||||
) : (
|
||||
<>测试模板</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</Form>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { Tabs, Button } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Plus, ArrowRight } from "lucide-react";
|
||||
import DataAnnotation from "../DataAnnotation/Annotate/components/TextAnnotation";
|
||||
import { useNavigate } from "react-router";
|
||||
import InstructionTemplateTab from "./components/InstructionTemplateTab";
|
||||
import SynthesisTaskTab from "./components/SynthesisTaskTab";
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
|
||||
export default function DataSynthesisPage() {
|
||||
return <DevelopmentInProgress showTime="2025.11.30" />;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [activeTab, setActiveTab] = useState("tasks");
|
||||
@@ -40,45 +39,42 @@ export default function DataSynthesisPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className=" p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-bold text-gray-900">数据合成</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate("/data/synthesis/task/create-template");
|
||||
}}
|
||||
>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
创建模板
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => navigate("/data/synthesis/task/create")}
|
||||
>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
创建合成任务
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-bold text-gray-900">数据合成</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate("/data/synthesis/task/create-template");
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
创建模板
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => navigate("/data/synthesis/task/create")}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
创建合成任务
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{ key: "tasks", label: "合成任务", children: <SynthesisTaskTab /> },
|
||||
{
|
||||
key: "templates",
|
||||
label: "指令模板",
|
||||
children: <InstructionTemplateTab />,
|
||||
},
|
||||
]}
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
></Tabs>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{ key: "tasks", label: "合成任务", children: <SynthesisTaskTab /> },
|
||||
{
|
||||
key: "templates",
|
||||
label: "指令模板",
|
||||
children: <InstructionTemplateTab />,
|
||||
},
|
||||
]}
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
></Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { Card, Table, Badge, Button } from "antd";
|
||||
import {
|
||||
Plus,
|
||||
FileText,
|
||||
Search,
|
||||
Edit,
|
||||
Copy,
|
||||
Trash2,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
import { EditOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import { Plus, FileText } from "lucide-react";
|
||||
import type { Template } from "@/pages/SynthesisTask/synthesis";
|
||||
import { useNavigate } from "react-router";
|
||||
import { mockTemplates } from "@/mock/synthesis";
|
||||
@@ -45,6 +38,7 @@ export default function InstructionTemplateTab() {
|
||||
title: "模板名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
render: (text: string, template: Template) => (
|
||||
<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">
|
||||
@@ -103,14 +97,7 @@ export default function InstructionTemplateTab() {
|
||||
title: "质量评分",
|
||||
dataIndex: "quality",
|
||||
key: "quality",
|
||||
render: (quality: number) =>
|
||||
quality ? (
|
||||
<Badge className={`font-medium text-xs ${getQualityColor(quality)}`}>
|
||||
{quality}%
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400">-</span>
|
||||
),
|
||||
render: (quality: number) => (quality ? `${quality}%` : "-"),
|
||||
},
|
||||
{
|
||||
title: "最后使用",
|
||||
@@ -123,25 +110,14 @@ export default function InstructionTemplateTab() {
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
align: "center" as const,
|
||||
fixed: "right",
|
||||
render: (_: any, template: Template) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
onClick={() =>
|
||||
navigate(`/data/synthesis/task/create-template/${template.id}`)
|
||||
}
|
||||
type="text"
|
||||
>
|
||||
<Edit className="w-3 h-3" />
|
||||
</Button>
|
||||
<div className="flex items-center justify-center">
|
||||
<Button type="text">
|
||||
<Copy className="w-3 h-3" />
|
||||
<EditOutlined />
|
||||
</Button>
|
||||
<Button type="text">
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button type="text">
|
||||
<MoreHorizontal className="w-3 h-3" />
|
||||
<Button type="text" danger>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -9,11 +9,14 @@ import {
|
||||
Play,
|
||||
DownloadIcon,
|
||||
CheckCircle,
|
||||
Check,
|
||||
StopCircle,
|
||||
} from "lucide-react";
|
||||
import type { SynthesisTask } from "@/pages/SynthesisTask/synthesis";
|
||||
import { mockSynthesisTasks } from "@/mock/synthesis";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { formatDateTime } from "@/utils/unit";
|
||||
|
||||
export default function SynthesisTaskTab() {
|
||||
const navigate = useNavigate();
|
||||
@@ -73,28 +76,28 @@ export default function SynthesisTaskTab() {
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: {
|
||||
span: "等待中",
|
||||
color: "bg-yellow-50 text-yellow-700 border-yellow-200",
|
||||
label: "等待中",
|
||||
color: "#F59E0B",
|
||||
icon: Pause,
|
||||
},
|
||||
running: {
|
||||
span: "运行中",
|
||||
color: "bg-blue-50 text-blue-700 border-blue-200",
|
||||
label: "运行中",
|
||||
color: "#3B82F6",
|
||||
icon: Play,
|
||||
},
|
||||
completed: {
|
||||
span: "已完成",
|
||||
color: "bg-green-50 text-green-700 border-green-200",
|
||||
label: "已完成",
|
||||
color: "#10B981",
|
||||
icon: CheckCircle,
|
||||
},
|
||||
failed: {
|
||||
span: "失败",
|
||||
color: "bg-red-50 text-red-700 border-red-200",
|
||||
label: "失败",
|
||||
color: "#EF4444",
|
||||
icon: Pause,
|
||||
},
|
||||
paused: {
|
||||
span: "已暂停",
|
||||
color: "bg-gray-50 text-gray-700 border-gray-200",
|
||||
label: "已暂停",
|
||||
color: "#E5E7EB",
|
||||
icon: Pause,
|
||||
},
|
||||
};
|
||||
@@ -130,6 +133,7 @@ export default function SynthesisTaskTab() {
|
||||
),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
render: (text: string, task: SynthesisTask) => (
|
||||
<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">
|
||||
@@ -139,7 +143,7 @@ export default function SynthesisTaskTab() {
|
||||
</span>
|
||||
</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>
|
||||
</div>
|
||||
@@ -149,11 +153,7 @@ export default function SynthesisTaskTab() {
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
render: (type: string) => (
|
||||
<Badge className="bg-blue-50 text-blue-700 border-blue-200 text-xs">
|
||||
{type?.toUpperCase()}
|
||||
</Badge>
|
||||
),
|
||||
render: (type: string) => type.toUpperCase(),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
@@ -161,38 +161,17 @@ export default function SynthesisTaskTab() {
|
||||
key: "status",
|
||||
render: (status: string) => {
|
||||
const statusConfig = getStatusBadge(status);
|
||||
const StatusIcon = statusConfig.icon;
|
||||
return (
|
||||
<Badge
|
||||
className={`${statusConfig.color} flex items-center gap-1 w-fit text-xs`}
|
||||
>
|
||||
<StatusIcon className="w-3 h-3" />
|
||||
{statusConfig.span}
|
||||
</Badge>
|
||||
);
|
||||
return <Badge color={statusConfig.color} text={statusConfig.label} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "进度",
|
||||
dataIndex: "progress",
|
||||
key: "progress",
|
||||
render: (_: any, task: SynthesisTask) =>
|
||||
task.status === "running" ? (
|
||||
<div className="space-y-1">
|
||||
<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>
|
||||
),
|
||||
width: 150,
|
||||
render: (_: any, task: SynthesisTask) => (
|
||||
<Progress percent={task.progress} size="small" />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "源数据集",
|
||||
@@ -217,48 +196,18 @@ export default function SynthesisTaskTab() {
|
||||
title: "质量评分",
|
||||
dataIndex: "quality",
|
||||
key: "quality",
|
||||
render: (quality: number) =>
|
||||
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>
|
||||
),
|
||||
render: (quality: number) => (quality ? `${quality}%` : "-"),
|
||||
},
|
||||
{
|
||||
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>
|
||||
),
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
render: (createdAt: string) => (
|
||||
<div className="text-sm text-gray-600">{createdAt}</div>
|
||||
),
|
||||
render: formatDateTime,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
align: "center" as const,
|
||||
fixed: "right" as const,
|
||||
render: (_: any, task: SynthesisTask) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{task.status === "running" && (
|
||||
@@ -266,36 +215,24 @@ export default function SynthesisTaskTab() {
|
||||
onClick={() => handleTaskAction(task.id, "pause")}
|
||||
className="hover:bg-orange-50 p-1 h-7 w-7"
|
||||
type="text"
|
||||
>
|
||||
<Pause className="w-3 h-3" />
|
||||
</Button>
|
||||
icon={<Pause className="w-4 h-4" />}
|
||||
></Button>
|
||||
)}
|
||||
{task.status === "paused" && (
|
||||
<Button
|
||||
onClick={() => handleTaskAction(task.id, "resume")}
|
||||
className="hover:bg-green-50 p-1 h-7 w-7"
|
||||
type="text"
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
</Button>
|
||||
icon={<Play className="w-4 h-4" />}
|
||||
></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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="space-y-4">
|
||||
{/* 搜索和筛选 */}
|
||||
<SearchControls
|
||||
searchTerm={searchQuery}
|
||||
|
||||
Reference in New Issue
Block a user