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:
chenghh-9609
2025-11-20 14:12:59 +08:00
committed by GitHub
parent a07fba23f2
commit 955ffff6cd
8 changed files with 755 additions and 1264 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
), ),

View File

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