add operator create page (#38)

* feat: Update site name to DataMate and refine text for AI data processing

* feat: Refactor settings page and implement model access functionality

- Created a new ModelAccess component for managing model configurations.
- Removed the old Settings component and replaced it with a new SettingsPage component that integrates ModelAccess, SystemConfig, and WebhookConfig.
- Added SystemConfig component for managing system settings.
- Implemented WebhookConfig component for managing webhook configurations.
- Updated API functions for model management in settings.apis.ts.
- Adjusted routing to point to the new SettingsPage component.

* feat: Implement Data Collection Page with Task Management and Execution Log

- Created DataCollectionPage component to manage data collection tasks.
- Added TaskManagement and ExecutionLog components for task handling and logging.
- Integrated task operations including start, stop, edit, and delete functionalities.
- Implemented filtering and searching capabilities in task management.
- Introduced SimpleCronScheduler for scheduling tasks with cron expressions.
- Updated CreateTask component to utilize new scheduling and template features.
- Enhanced BasicInformation component to conditionally render fields based on visibility settings.
- Refactored ImportConfiguration component to remove NAS import section.

* feat: Update task creation API endpoint and enhance task creation form with new fields and validation

* Refactor file upload and operator management components

- Removed unnecessary console logs from file download and export functions.
- Added size property to TaskItem interface for better task management.
- Simplified TaskUpload component by utilizing useFileSliceUpload hook for file upload logic.
- Enhanced OperatorPluginCreate component to handle file uploads and parsing more efficiently.
- Updated ConfigureStep component to use Ant Design Form for better data handling and validation.
- Improved PreviewStep component to navigate back to the operator market.
- Added support for additional file types in UploadStep component.
- Implemented delete operator functionality in OperatorMarketPage with confirmation prompts.
- Cleaned up unused API functions in operator.api.ts to streamline the codebase.
- Fixed number formatting utility to handle zero values correctly.
This commit is contained in:
chenghh-9609
2025-10-30 16:30:01 +08:00
committed by GitHub
parent e0884ab048
commit 5612c7cd91
22 changed files with 640 additions and 979 deletions

View File

@@ -1,4 +1,4 @@
import { Button, Steps } from "antd";
import { Button, App, Steps } from "antd";
import {
ArrowLeft,
CheckCircle,
@@ -6,122 +6,102 @@ import {
TagIcon,
Upload,
} from "lucide-react";
import { useNavigate } from "react-router";
import { useCallback, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { useEffect, useState } from "react";
import UploadStep from "./components/UploadStep";
import ParsingStep from "./components/ParsingStep";
import ConfigureStep from "./components/ConfigureStep";
import PreviewStep from "./components/PreviewStep";
interface ParsedOperatorInfo {
name: string;
version: string;
description: string;
author: string;
category: string;
modality: string[];
type: "preprocessing" | "training" | "inference" | "postprocessing";
framework: string;
language: string;
size: string;
dependencies: string[];
inputFormat: string[];
outputFormat: string[];
performance: {
accuracy?: number;
speed: string;
memory: string;
};
documentation?: string;
examples?: string[];
}
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
import {
createOperatorUsingPost,
preUploadOperatorUsingPost,
queryOperatorByIdUsingGet,
updateOperatorByIdUsingPut,
uploadOperatorChunkUsingPost,
uploadOperatorUsingPost,
} from "../operator.api";
import { sliceFile } from "@/utils/file.util";
export default function OperatorPluginCreate() {
const navigate = useNavigate();
const { id } = useParams();
const { message } = App.useApp();
const [uploadStep, setUploadStep] = useState<
"upload" | "parsing" | "configure" | "preview"
>("upload");
const [isUploading, setIsUploading] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
const [parseProgress, setParseProgress] = useState(0);
const [parsedInfo, setParsedInfo] = useState<ParsedOperatorInfo | null>(null);
const [parsedInfo, setParsedInfo] = useState({});
const [parseError, setParseError] = useState<string | null>(null);
const { handleUpload, createTask, taskList } = useFileSliceUpload(
{
preUpload: preUploadOperatorUsingPost,
uploadChunk: uploadOperatorChunkUsingPost,
cancelUpload: null,
},
false
);
// 模拟文件上传
const handleFileUpload = useCallback((files: FileList) => {
const handleFileUpload = async (files: FileList) => {
setIsUploading(true);
setParseError(null);
// 模拟文件上传过程
setTimeout(() => {
const fileArray = Array.from(files).map((file) => ({
name: file.name,
size: file.size,
type: file.type,
}));
setUploadedFiles(fileArray);
setIsUploading(false);
setUploadStep("parsing");
startParsing();
}, 1000);
}, []);
// 模拟解析过程
const startParsing = useCallback(() => {
setParseProgress(0);
const interval = setInterval(() => {
setParseProgress((prev) => {
if (prev >= 100) {
clearInterval(interval);
// 模拟解析完成
setTimeout(() => {
setParsedInfo({
name: "图像预处理算子",
version: "1.2.0",
description:
"支持图像缩放、裁剪、旋转、颜色空间转换等常用预处理操作,优化了内存使用和处理速度",
author: "当前用户",
category: "图像处理",
modality: ["image"],
type: "preprocessing",
framework: "PyTorch",
language: "Python",
size: "2.3MB",
dependencies: [
"opencv-python>=4.5.0",
"pillow>=8.0.0",
"numpy>=1.20.0",
],
inputFormat: ["jpg", "png", "bmp", "tiff"],
outputFormat: ["jpg", "png", "tensor"],
performance: {
accuracy: 99.5,
speed: "50ms/image",
memory: "128MB",
},
documentation:
"# 图像预处理算子\n\n这是一个高效的图像预处理算子...",
examples: [
"from operator import ImagePreprocessor\nprocessor = ImagePreprocessor()\nresult = processor.process(image)",
],
});
setUploadStep("configure");
}, 500);
return 100;
}
return prev + 10;
setUploadStep("parsing");
try {
const fileName = files[0].name;
await handleUpload({
task: createTask({
dataset: { id: "operator-upload", name: "上传算子" },
}),
files: [
{
originFile: files[0],
slices: sliceFile(files[0]),
name: fileName,
size: files[0].size,
},
], // 假设只上传一个文件
});
}, 200);
}, []);
const handlePublish = () => {
// 模拟发布过程
setUploadStep("preview");
setTimeout(() => {
alert("算子发布成功!");
// 这里可以重置状态或跳转到其他页面
}, 2000);
setParsedInfo({ ...parsedInfo, fileName, percent: 100 }); // 上传完成,进度100%
// 解析文件过程
const res = await uploadOperatorUsingPost({ fileName });
setParsedInfo({ ...parsedInfo, ...res.data });
} catch (err) {
setParseError("文件解析失败," + err.data.message);
} finally {
setIsUploading(false);
setUploadStep("configure");
}
};
const handlePublish = async () => {
try {
if (id) {
await updateOperatorByIdUsingPut(id, parsedInfo!);
} else {
await createOperatorUsingPost(parsedInfo);
}
setUploadStep("preview");
} catch (err) {
message.error("算子发布失败," + err.data.message);
}
};
const onFetchOperator = async (operatorId: string) => {
// 编辑模式,加载已有算子信息逻辑待实现
const { data } = await queryOperatorByIdUsingGet(operatorId);
setParsedInfo(data);
setUploadStep("configure");
};
useEffect(() => {
if (id) {
// 编辑模式,加载已有算子信息逻辑待实现
onFetchOperator(id);
}
}, [id]);
return (
<div className="flex-overflow-auto bg-gray-50">
{/* Header */}
@@ -174,13 +154,13 @@ export default function OperatorPluginCreate() {
)}
{uploadStep === "parsing" && (
<ParsingStep
parseProgress={parseProgress}
uploadedFiles={uploadedFiles}
parseProgress={taskList[0]?.percent || parsedInfo.percent || 0}
uploadedFiles={taskList}
/>
)}
{uploadStep === "configure" && (
<ConfigureStep
setUploadStep={setUploadStep}
setParsedInfo={setParsedInfo}
parseError={parseError}
parsedInfo={parsedInfo}
/>
@@ -192,7 +172,6 @@ export default function OperatorPluginCreate() {
{uploadStep === "configure" && (
<div className="flex justify-end gap-3 mt-8">
<Button onClick={() => setUploadStep("upload")}></Button>
<Button onClick={() => setUploadStep("preview")}></Button>
<Button type="primary" onClick={handlePublish}>
</Button>

View File

@@ -1,274 +1,75 @@
import { Alert, Input, Button } from "antd";
import { CheckCircle, Plus, TagIcon, X } from "lucide-react";
import { useState } from "react";
export default function ConfigureStep({ parsedInfo, parseError }) {
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [customTag, setCustomTag] = useState("");
const availableTags = [
"图像处理",
"预处理",
"缩放",
"裁剪",
"旋转",
"文本处理",
"分词",
"中文",
"NLP",
"医学",
"音频处理",
"特征提取",
"MFCC",
"频谱分析",
"视频处理",
"帧提取",
"关键帧",
"采样",
"多模态",
"融合",
"深度学习",
"注意力机制",
"推理加速",
"TensorRT",
"优化",
"GPU",
"数据增强",
"几何变换",
"颜色变换",
"噪声",
];
const handleAddCustomTag = () => {
if (customTag.trim() && !selectedTags.includes(customTag.trim())) {
setSelectedTags([...selectedTags, customTag.trim()]);
setCustomTag("");
}
};
const handleRemoveTag = (tagToRemove: string) => {
setSelectedTags(selectedTags.filter((tag) => tag !== tagToRemove));
};
import { Alert, Input, Form } from "antd";
import TextArea from "antd/es/input/TextArea";
export default function ConfigureStep({
parsedInfo,
parseError,
setParsedInfo,
}) {
return (
<>
{/* 解析结果 */}
<div className="flex items-center gap-3 mb-6">
<CheckCircle className="w-6 h-6 text-green-500" />
<h2 className="text-xl font-bold text-gray-900"></h2>
</div>
{parseError && (
<Alert
message="解析过程中发现问题"
description={parseError}
type="warning"
type="error"
showIcon
className="mb-6"
/>
)}
{parsedInfo && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Form
layout="vertical"
initialValues={parsedInfo}
onValuesChange={(_, allValues) => {
setParsedInfo({ ...parsedInfo, ...allValues });
}}
>
{/* 基本信息 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900"></h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-2 bg-gray-50 rounded border text-gray-900">
{parsedInfo.name}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-2 bg-gray-50 rounded border text-gray-900">
{parsedInfo.version}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-2 bg-gray-50 rounded border text-gray-900">
{parsedInfo.author}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-2 bg-gray-50 rounded border text-gray-900">
{parsedInfo.category}
</div>
</div>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900"></h3>
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
<Input value={parsedInfo.id} readOnly />
</Form.Item>
<Form.Item label="名称" name="name" rules={[{ required: true }]}>
<Input value={parsedInfo.name} />
</Form.Item>
<Form.Item label="版本" name="version" rules={[{ required: true }]}>
<Input value={parsedInfo.version} />
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: false }]}
>
<TextArea value={parsedInfo.description} />
</Form.Item>
{/* 技术规格 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900"></h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-2 bg-gray-50 rounded border text-gray-900">
{parsedInfo.framework}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-2 bg-gray-50 rounded border text-gray-900">
{parsedInfo.language}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-2 bg-gray-50 rounded border text-gray-900">
{parsedInfo.type}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-2 bg-gray-50 rounded border text-gray-900">
{parsedInfo.modality.join(", ")}
</div>
</div>
</div>
</div>
{/* 描述 */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-3 bg-gray-50 rounded border text-gray-900">
{parsedInfo.description}
</div>
</div>
{/* 依赖项 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-3 bg-gray-50 rounded border">
<div className="space-y-1">
{parsedInfo.dependencies.map((dep, index) => (
<div key={index} className="text-sm text-gray-900 font-mono">
{dep}
</div>
))}
</div>
</div>
</div>
{/* 性能指标 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="p-3 bg-gray-50 rounded border space-y-2">
{parsedInfo.performance.accuracy && (
<div className="text-sm">
<span className="font-medium">:</span>{" "}
{parsedInfo.performance.accuracy}%
</div>
)}
<div className="text-sm">
<span className="font-medium">:</span>{" "}
{parsedInfo.performance.speed}
</div>
<div className="text-sm">
<span className="font-medium">:</span>{" "}
{parsedInfo.performance.memory}
</div>
</div>
</div>
</div>
)}
{/* 标签配置 */}
{/* 预定义标签 */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3"></h3>
<div className="flex flex-wrap gap-2">
{availableTags.map((tag) => (
<button
key={tag}
onClick={() => {
if (selectedTags.includes(tag)) {
handleRemoveTag(tag);
} else {
setSelectedTags([...selectedTags, tag]);
}
}}
className={`px-3 py-1 rounded-full text-sm font-medium border transition-colors ${
selectedTags.includes(tag)
? "bg-blue-100 text-blue-800 border-blue-200"
: "bg-gray-50 text-gray-700 border-gray-200 hover:bg-gray-100"
}`}
>
{tag}
</button>
))}
</div>
</div>
{/* 自定义标签 */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3">
</h3>
<div className="flex gap-2">
<Input
placeholder="输入自定义标签..."
value={customTag}
onChange={(e) => setCustomTag(e.target.value)}
onPressEnter={handleAddCustomTag}
className="flex-1"
/>
<Button onClick={handleAddCustomTag} disabled={!customTag.trim()}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 已选标签 */}
{selectedTags.length > 0 && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
({selectedTags.length})
<h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
</h3>
<div className="flex flex-wrap gap-2">
{selectedTags.map((tag) => (
<div
key={tag}
className="flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium"
>
<TagIcon className="w-3 h-3" />
<span>{tag}</span>
<button
onClick={() => handleRemoveTag(tag)}
className="ml-1 hover:text-blue-600"
>
<X className="w-3 h-3" />
</button>
</div>
))}
<div className="border p-4 rounded-lg flex items-center justify-between gap-4">
<div className="flex-1">
<span className="bg-[#2196f3] border-radius px-4 py-1 rounded-tl-lg rounded-br-lg text-white">
</span>
<pre className="p-4 text-sm overflow-auto">
{parsedInfo.inputs}
</pre>
</div>
<h1 className="text-3xl">VS</h1>
<div className="flex-1">
<span className="bg-[#4caf50] border-radius px-4 py-1 rounded-tl-lg rounded-br-lg text-white">
</span>
<pre className=" p-4 text-sm overflow-auto">
{parsedInfo.outputs}
</pre>
</div>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 mt-8"></h3>
</Form>
)}
</>
);

View File

@@ -1,7 +1,9 @@
import { Button } from "antd";
import { CheckCircle, Plus, Eye } from "lucide-react";
import { CheckCircle, Plus } from "lucide-react";
import { useNavigate } from "react-router";
export default function PreviewStep({ setUploadStep }) {
const navigate = useNavigate();
return (
<div className="text-center py-2">
<div className="w-24 h-24 mx-auto mb-6 bg-green-50 rounded-full flex items-center justify-center">
@@ -15,9 +17,8 @@ export default function PreviewStep({ setUploadStep }) {
<Plus className="w-4 h-4 mr-2" />
</Button>
<Button type="primary">
<Eye className="w-4 h-4 mr-2" />
<Button type="primary" onClick={() => navigate("/data/operator-market")}>
</Button>
</div>
</div>

View File

@@ -6,6 +6,7 @@ export default function UploadStep({ isUploading, onUpload }) {
{ ext: ".py", desc: "Python 脚本文件" },
{ ext: ".zip", desc: "压缩包文件" },
{ ext: ".tar.gz", desc: "压缩包文件" },
{ ext: ".tar", desc: "压缩包文件" },
{ ext: ".whl", desc: "Python Wheel 包" },
{ ext: ".yaml", desc: "配置文件" },
{ ext: ".yml", desc: "配置文件" },