feat: 创建符合ComfyUI SDK规范的工作流模板创建器
- 创建了新的WorkflowTemplateCreator组件,专门用于创建符合ComfyUI SDK规范的WorkflowTemplateData格式模板 - 支持模板元数据配置(ID、名称、描述、版本、作者、分类、标签) - 支持ComfyUI工作流JSON导入和编辑 - 支持参数配置(预留接口) - 实现了完整的表单验证和错误处理 - 自动生成模板ID功能 - 为后续与ComfyUI SDK模板系统集成做准备 主要特性: - ✅ 符合ComfyUI SDK的WorkflowTemplateData格式 - ✅ 完整的模板元数据管理 - ✅ JSON文件导入功能 - ✅ 表单验证和错误提示 - ✅ 自动ID生成 - ✅ 响应式设计
This commit is contained in:
@@ -44,8 +44,8 @@
|
|||||||
"@testing-library/jest-dom": "^6.1.5",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
"@testing-library/react": "^14.1.2",
|
"@testing-library/react": "^14.1.2",
|
||||||
"@testing-library/user-event": "^14.5.1",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.23",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { useComfyUIV2Store, useFilteredWorkflows } from '../../store/comfyuiV2Store';
|
import { useComfyUIV2Store, useFilteredWorkflows } from '../../store/comfyuiV2Store';
|
||||||
import type { WorkflowV2 } from '../../services/comfyuiV2Service';
|
import type { WorkflowV2 } from '../../services/comfyuiV2Service';
|
||||||
import { WorkflowV2Creator } from './WorkflowV2Creator';
|
import { WorkflowTemplateCreator } from './WorkflowTemplateCreator';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
interface WorkflowManagerProps {
|
interface WorkflowManagerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
518
apps/desktop/src/components/comfyui/WorkflowTemplateCreator.tsx
Normal file
518
apps/desktop/src/components/comfyui/WorkflowTemplateCreator.tsx
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
/**
|
||||||
|
* ComfyUI V2 工作流模板创建器组件
|
||||||
|
* 专门用于创建符合ComfyUI SDK规范的WorkflowTemplateData格式的模板
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
Save,
|
||||||
|
Upload,
|
||||||
|
FileText,
|
||||||
|
Settings,
|
||||||
|
Info,
|
||||||
|
AlertCircle,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
// 定义符合ComfyUI SDK的模板数据结构
|
||||||
|
interface TemplateMetadata {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
version?: string;
|
||||||
|
author?: string;
|
||||||
|
tags?: string[];
|
||||||
|
category?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParameterSchema {
|
||||||
|
param_type: 'string' | 'number' | 'boolean' | 'array' | 'object';
|
||||||
|
required?: boolean;
|
||||||
|
default?: any;
|
||||||
|
description?: string;
|
||||||
|
enum?: any[];
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
pattern?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkflowTemplateData {
|
||||||
|
metadata: TemplateMetadata;
|
||||||
|
workflow: any; // ComfyUI workflow JSON
|
||||||
|
parameters: Record<string, ParameterSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateTemplateRequest {
|
||||||
|
name: string;
|
||||||
|
category?: string;
|
||||||
|
description?: string;
|
||||||
|
template_data: WorkflowTemplateData;
|
||||||
|
parameter_schema: Record<string, ParameterSchema>;
|
||||||
|
tags: string[];
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkflowTemplateCreatorProps {
|
||||||
|
/** 是否显示模态框 */
|
||||||
|
isOpen: boolean;
|
||||||
|
/** 关闭回调 */
|
||||||
|
onClose: () => void;
|
||||||
|
/** 保存回调 */
|
||||||
|
onSave: (templateData: CreateTemplateRequest) => Promise<void>;
|
||||||
|
/** 编辑的模板(为空时表示创建新模板) */
|
||||||
|
editingTemplate?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ComfyUI V2 工作流模板创建器组件
|
||||||
|
*/
|
||||||
|
export const WorkflowTemplateCreator: React.FC<WorkflowTemplateCreatorProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
editingTemplate
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<'basic' | 'workflow' | 'parameters'>('basic');
|
||||||
|
const [templateData, setTemplateData] = useState<WorkflowTemplateData>({
|
||||||
|
metadata: {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
version: '1.0.0',
|
||||||
|
author: '',
|
||||||
|
tags: [],
|
||||||
|
category: '',
|
||||||
|
},
|
||||||
|
workflow: {},
|
||||||
|
parameters: {},
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [workflowJsonText, setWorkflowJsonText] = useState('{}');
|
||||||
|
|
||||||
|
// 重置表单数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
if (editingTemplate) {
|
||||||
|
setTemplateData({
|
||||||
|
metadata: {
|
||||||
|
id: editingTemplate.id || '',
|
||||||
|
name: editingTemplate.name || '',
|
||||||
|
description: editingTemplate.description || '',
|
||||||
|
version: editingTemplate.version || '1.0.0',
|
||||||
|
author: editingTemplate.author || '',
|
||||||
|
tags: editingTemplate.tags || [],
|
||||||
|
category: editingTemplate.category || '',
|
||||||
|
},
|
||||||
|
workflow: editingTemplate.workflow_data || {},
|
||||||
|
parameters: editingTemplate.parameters || {},
|
||||||
|
});
|
||||||
|
setWorkflowJsonText(JSON.stringify(editingTemplate.workflow_data || {}, null, 2));
|
||||||
|
} else {
|
||||||
|
// 重置为默认值
|
||||||
|
setTemplateData({
|
||||||
|
metadata: {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
version: '1.0.0',
|
||||||
|
author: '',
|
||||||
|
tags: [],
|
||||||
|
category: '',
|
||||||
|
},
|
||||||
|
workflow: {},
|
||||||
|
parameters: {},
|
||||||
|
});
|
||||||
|
setWorkflowJsonText('{}');
|
||||||
|
}
|
||||||
|
setErrors({});
|
||||||
|
setActiveTab('basic');
|
||||||
|
}
|
||||||
|
}, [editingTemplate, isOpen]);
|
||||||
|
|
||||||
|
// 更新模板元数据字段
|
||||||
|
const updateMetadata = (field: keyof TemplateMetadata, value: any) => {
|
||||||
|
setTemplateData(prev => ({
|
||||||
|
...prev,
|
||||||
|
metadata: { ...prev.metadata, [field]: value }
|
||||||
|
}));
|
||||||
|
// 清除该字段的错误
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors(prev => {
|
||||||
|
const newErrors = { ...prev };
|
||||||
|
delete newErrors[field];
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证表单
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (!templateData.metadata.name.trim()) {
|
||||||
|
newErrors.name = '模板名称不能为空';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!templateData.metadata.id.trim()) {
|
||||||
|
newErrors.id = '模板ID不能为空';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workflowJsonText.trim() || workflowJsonText.trim() === '{}') {
|
||||||
|
newErrors.workflow = '工作流数据不能为空';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(workflowJsonText);
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
|
newErrors.workflow = '工作流数据必须是有效的JSON对象';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
newErrors.workflow = 'JSON格式错误,请检查语法';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理保存
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const workflowData = JSON.parse(workflowJsonText);
|
||||||
|
|
||||||
|
// 构建符合ComfyUI SDK规范的模板数据
|
||||||
|
const finalTemplateData: WorkflowTemplateData = {
|
||||||
|
...templateData,
|
||||||
|
workflow: workflowData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestData: CreateTemplateRequest = {
|
||||||
|
name: templateData.metadata.name,
|
||||||
|
category: templateData.metadata.category,
|
||||||
|
description: templateData.metadata.description,
|
||||||
|
template_data: finalTemplateData,
|
||||||
|
parameter_schema: templateData.parameters,
|
||||||
|
tags: templateData.metadata.tags || [],
|
||||||
|
author: templateData.metadata.author,
|
||||||
|
};
|
||||||
|
|
||||||
|
await onSave(requestData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存模板失败:', error);
|
||||||
|
setErrors({ general: `保存失败: ${error}` });
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导入ComfyUI工作流
|
||||||
|
const handleImportWorkflow = () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const workflow = JSON.parse(e.target?.result as string);
|
||||||
|
setWorkflowJsonText(JSON.stringify(workflow, null, 2));
|
||||||
|
setTemplateData(prev => ({ ...prev, workflow }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析ComfyUI工作流失败:', error);
|
||||||
|
setErrors({ workflow: '文件格式错误,请选择有效的JSON文件' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理标签输入
|
||||||
|
const handleTagsChange = (tagsString: string) => {
|
||||||
|
const tags = tagsString
|
||||||
|
.split(',')
|
||||||
|
.map(tag => tag.trim())
|
||||||
|
.filter(tag => tag.length > 0);
|
||||||
|
updateMetadata('tags', tags);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成模板ID
|
||||||
|
const generateTemplateId = (name: string) => {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\s]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理名称变化,自动生成ID
|
||||||
|
const handleNameChange = (name: string) => {
|
||||||
|
updateMetadata('name', name);
|
||||||
|
if (!editingTemplate) {
|
||||||
|
updateMetadata('id', generateTemplateId(name));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[95vh] flex flex-col">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Settings className="w-6 h-6 text-blue-600" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
|
{editingTemplate ? '编辑工作流模板' : '创建工作流模板'}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{errors.general && (
|
||||||
|
<div className="mx-6 mt-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center space-x-2">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||||
|
<span className="text-red-700">{errors.general}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 标签页导航 */}
|
||||||
|
<div className="flex border-b border-gray-200 px-6">
|
||||||
|
{[
|
||||||
|
{ id: 'basic', label: '基本信息', icon: FileText },
|
||||||
|
{ id: 'workflow', label: '工作流配置', icon: Settings },
|
||||||
|
{ id: 'parameters', label: '参数配置', icon: Info },
|
||||||
|
].map(tab => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id as any)}
|
||||||
|
className={`flex items-center space-x-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
{/* 基本信息标签页 */}
|
||||||
|
{activeTab === 'basic' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
模板名称 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.metadata.name}
|
||||||
|
onChange={(e) => handleNameChange(e.target.value)}
|
||||||
|
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
|
errors.name ? 'border-red-300' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
placeholder="例如:AI模型面部和头发细节修复"
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
模板ID <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.metadata.id}
|
||||||
|
onChange={(e) => updateMetadata('id', e.target.value)}
|
||||||
|
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
|
errors.id ? 'border-red-300' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
placeholder="例如:ai-model-face-hair-fix"
|
||||||
|
/>
|
||||||
|
{errors.id && (
|
||||||
|
<p className="mt-1 text-sm text-red-600">{errors.id}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
描述
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={templateData.metadata.description || ''}
|
||||||
|
onChange={(e) => updateMetadata('description', e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="描述这个模板的功能和用途..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
分类
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.metadata.category || ''}
|
||||||
|
onChange={(e) => updateMetadata('category', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="例如:enhancement"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
版本
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.metadata.version || ''}
|
||||||
|
onChange={(e) => updateMetadata('version', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="例如:1.0.0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
作者
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.metadata.author || ''}
|
||||||
|
onChange={(e) => updateMetadata('author', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="例如:ComfyUI SDK"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
标签
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.metadata.tags?.join(', ') || ''}
|
||||||
|
onChange={(e) => handleTagsChange(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="用逗号分隔,例如:face-enhancement, hair-enhancement, portrait"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">用逗号分隔多个标签</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 工作流配置标签页 */}
|
||||||
|
{activeTab === 'workflow' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">ComfyUI工作流配置</h3>
|
||||||
|
<button
|
||||||
|
onClick={handleImportWorkflow}
|
||||||
|
className="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
<span>导入JSON</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
工作流JSON <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={workflowJsonText}
|
||||||
|
onChange={(e) => setWorkflowJsonText(e.target.value)}
|
||||||
|
rows={20}
|
||||||
|
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm ${
|
||||||
|
errors.workflow ? 'border-red-300' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
placeholder="粘贴ComfyUI导出的工作流JSON..."
|
||||||
|
/>
|
||||||
|
{errors.workflow && (
|
||||||
|
<p className="mt-1 text-sm text-red-600">{errors.workflow}</p>
|
||||||
|
)}
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
从ComfyUI界面导出工作流JSON并粘贴到这里
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 参数配置标签页 */}
|
||||||
|
{activeTab === 'parameters' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Info className="w-5 h-5 text-blue-600" />
|
||||||
|
<h3 className="text-sm font-medium text-blue-900">参数配置</h3>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm text-blue-700">
|
||||||
|
参数配置功能正在开发中,敬请期待...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部操作栏 */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-t border-gray-200">
|
||||||
|
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||||
|
<Info className="w-4 h-4" />
|
||||||
|
<span>保存后将创建符合ComfyUI SDK规范的模板</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Settings className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span>{isSaving ? '保存中...' : (editingTemplate ? '更新' : '创建')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* ComfyUI V2 工作流创建器组件
|
* ComfyUI V2 工作流模板创建器组件
|
||||||
* 专门用于创建ComfyUI V2工作流的模态框
|
* 专门用于创建符合ComfyUI SDK规范的WorkflowTemplateData格式的模板
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
@@ -13,8 +13,47 @@ import {
|
|||||||
Info,
|
Info,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { CreateWorkflowRequest } from '../../services/comfyuiV2Service';
|
|
||||||
|
// 定义符合ComfyUI SDK的模板数据结构
|
||||||
|
interface TemplateMetadata {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
version?: string;
|
||||||
|
author?: string;
|
||||||
|
tags?: string[];
|
||||||
|
category?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParameterSchema {
|
||||||
|
param_type: 'string' | 'number' | 'boolean' | 'array' | 'object';
|
||||||
|
required?: boolean;
|
||||||
|
default?: any;
|
||||||
|
description?: string;
|
||||||
|
enum?: any[];
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
pattern?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkflowTemplateData {
|
||||||
|
metadata: TemplateMetadata;
|
||||||
|
workflow: any; // ComfyUI workflow JSON
|
||||||
|
parameters: Record<string, ParameterSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateTemplateRequest {
|
||||||
|
name: string;
|
||||||
|
category?: string;
|
||||||
|
description?: string;
|
||||||
|
template_data: WorkflowTemplateData;
|
||||||
|
parameter_schema: Record<string, ParameterSchema>;
|
||||||
|
tags: string[];
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface WorkflowV2CreatorProps {
|
interface WorkflowV2CreatorProps {
|
||||||
/** 是否显示模态框 */
|
/** 是否显示模态框 */
|
||||||
@@ -22,9 +61,9 @@ interface WorkflowV2CreatorProps {
|
|||||||
/** 关闭回调 */
|
/** 关闭回调 */
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
/** 保存回调 */
|
/** 保存回调 */
|
||||||
onSave: (workflowData: CreateWorkflowRequest) => Promise<void>;
|
onSave: (templateData: CreateTemplateRequest) => Promise<void>;
|
||||||
/** 编辑的工作流(为空时表示创建新工作流) */
|
/** 编辑的模板(为空时表示创建新模板) */
|
||||||
editingWorkflow?: any;
|
editingTemplate?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,48 +73,73 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
editingWorkflow
|
editingTemplate
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState<'basic' | 'workflow' | 'advanced'>('basic');
|
const [activeTab, setActiveTab] = useState<'basic' | 'workflow' | 'parameters'>('basic');
|
||||||
const [formData, setFormData] = useState<CreateWorkflowRequest>({
|
const [templateData, setTemplateData] = useState<WorkflowTemplateData>({
|
||||||
name: '',
|
metadata: {
|
||||||
description: '',
|
id: '',
|
||||||
workflow_json: {},
|
name: '',
|
||||||
tags: [],
|
description: '',
|
||||||
|
version: '1.0.0',
|
||||||
|
author: '',
|
||||||
|
tags: [],
|
||||||
|
category: '',
|
||||||
|
},
|
||||||
|
workflow: {},
|
||||||
|
parameters: {},
|
||||||
});
|
});
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [workflowJsonText, setWorkflowJsonText] = useState('{}');
|
const [workflowJsonText, setWorkflowJsonText] = useState('{}');
|
||||||
|
const [newParameterName, setNewParameterName] = useState('');
|
||||||
|
|
||||||
// 重置表单数据
|
// 重置表单数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
if (editingWorkflow) {
|
if (editingTemplate) {
|
||||||
setFormData({
|
setTemplateData({
|
||||||
name: editingWorkflow.name || '',
|
metadata: {
|
||||||
description: editingWorkflow.description || '',
|
id: editingTemplate.id || '',
|
||||||
workflow_json: editingWorkflow.workflow_data || {},
|
name: editingTemplate.name || '',
|
||||||
tags: editingWorkflow.tags || [],
|
description: editingTemplate.description || '',
|
||||||
|
version: editingTemplate.version || '1.0.0',
|
||||||
|
author: editingTemplate.author || '',
|
||||||
|
tags: editingTemplate.tags || [],
|
||||||
|
category: editingTemplate.category || '',
|
||||||
|
},
|
||||||
|
workflow: editingTemplate.workflow_data || {},
|
||||||
|
parameters: editingTemplate.parameters || {},
|
||||||
});
|
});
|
||||||
setWorkflowJsonText(JSON.stringify(editingWorkflow.workflow_data || {}, null, 2));
|
setWorkflowJsonText(JSON.stringify(editingTemplate.workflow_data || {}, null, 2));
|
||||||
} else {
|
} else {
|
||||||
// 重置为默认值
|
// 重置为默认值
|
||||||
setFormData({
|
setTemplateData({
|
||||||
name: '',
|
metadata: {
|
||||||
description: '',
|
id: '',
|
||||||
workflow_json: {},
|
name: '',
|
||||||
tags: [],
|
description: '',
|
||||||
|
version: '1.0.0',
|
||||||
|
author: '',
|
||||||
|
tags: [],
|
||||||
|
category: '',
|
||||||
|
},
|
||||||
|
workflow: {},
|
||||||
|
parameters: {},
|
||||||
});
|
});
|
||||||
setWorkflowJsonText('{}');
|
setWorkflowJsonText('{}');
|
||||||
}
|
}
|
||||||
setErrors({});
|
setErrors({});
|
||||||
setActiveTab('basic');
|
setActiveTab('basic');
|
||||||
}
|
}
|
||||||
}, [editingWorkflow, isOpen]);
|
}, [editingTemplate, isOpen]);
|
||||||
|
|
||||||
// 更新表单字段
|
// 更新模板元数据字段
|
||||||
const updateField = (field: keyof CreateWorkflowRequest, value: any) => {
|
const updateMetadata = (field: keyof TemplateMetadata, value: any) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setTemplateData(prev => ({
|
||||||
|
...prev,
|
||||||
|
metadata: { ...prev.metadata, [field]: value }
|
||||||
|
}));
|
||||||
// 清除该字段的错误
|
// 清除该字段的错误
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors(prev => {
|
setErrors(prev => {
|
||||||
@@ -86,24 +150,45 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 更新参数
|
||||||
|
const updateParameter = (paramName: string, schema: ParameterSchema) => {
|
||||||
|
setTemplateData(prev => ({
|
||||||
|
...prev,
|
||||||
|
parameters: { ...prev.parameters, [paramName]: schema }
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除参数
|
||||||
|
const removeParameter = (paramName: string) => {
|
||||||
|
setTemplateData(prev => {
|
||||||
|
const newParameters = { ...prev.parameters };
|
||||||
|
delete newParameters[paramName];
|
||||||
|
return { ...prev, parameters: newParameters };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 验证表单
|
// 验证表单
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
if (!formData.name.trim()) {
|
if (!templateData.metadata.name.trim()) {
|
||||||
newErrors.name = '工作流名称不能为空';
|
newErrors.name = '模板名称不能为空';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!templateData.metadata.id.trim()) {
|
||||||
|
newErrors.id = '模板ID不能为空';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!workflowJsonText.trim() || workflowJsonText.trim() === '{}') {
|
if (!workflowJsonText.trim() || workflowJsonText.trim() === '{}') {
|
||||||
newErrors.workflow_json = '工作流数据不能为空';
|
newErrors.workflow = '工作流数据不能为空';
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(workflowJsonText);
|
const parsed = JSON.parse(workflowJsonText);
|
||||||
if (typeof parsed !== 'object' || parsed === null) {
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
newErrors.workflow_json = '工作流数据必须是有效的JSON对象';
|
newErrors.workflow = '工作流数据必须是有效的JSON对象';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
newErrors.workflow_json = 'JSON格式错误,请检查语法';
|
newErrors.workflow = 'JSON格式错误,请检查语法';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,14 +205,26 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
const workflowData = JSON.parse(workflowJsonText);
|
const workflowData = JSON.parse(workflowJsonText);
|
||||||
const requestData: CreateWorkflowRequest = {
|
|
||||||
...formData,
|
// 构建符合ComfyUI SDK规范的模板数据
|
||||||
workflow_json: workflowData,
|
const finalTemplateData: WorkflowTemplateData = {
|
||||||
|
...templateData,
|
||||||
|
workflow: workflowData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestData: CreateTemplateRequest = {
|
||||||
|
name: templateData.metadata.name,
|
||||||
|
category: templateData.metadata.category,
|
||||||
|
description: templateData.metadata.description,
|
||||||
|
template_data: finalTemplateData,
|
||||||
|
parameter_schema: templateData.parameters,
|
||||||
|
tags: templateData.metadata.tags || [],
|
||||||
|
author: templateData.metadata.author,
|
||||||
};
|
};
|
||||||
|
|
||||||
await onSave(requestData);
|
await onSave(requestData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存工作流失败:', error);
|
console.error('保存模板失败:', error);
|
||||||
setErrors({ general: `保存失败: ${error}` });
|
setErrors({ general: `保存失败: ${error}` });
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
@@ -147,10 +244,10 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
|
|||||||
try {
|
try {
|
||||||
const workflow = JSON.parse(e.target?.result as string);
|
const workflow = JSON.parse(e.target?.result as string);
|
||||||
setWorkflowJsonText(JSON.stringify(workflow, null, 2));
|
setWorkflowJsonText(JSON.stringify(workflow, null, 2));
|
||||||
updateField('workflow_json', workflow);
|
setTemplateData(prev => ({ ...prev, workflow }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('解析ComfyUI工作流失败:', error);
|
console.error('解析ComfyUI工作流失败:', error);
|
||||||
setErrors({ workflow_json: '文件格式错误,请选择有效的JSON文件' });
|
setErrors({ workflow: '文件格式错误,请选择有效的JSON文件' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@@ -165,7 +262,7 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
|
|||||||
.split(',')
|
.split(',')
|
||||||
.map(tag => tag.trim())
|
.map(tag => tag.trim())
|
||||||
.filter(tag => tag.length > 0);
|
.filter(tag => tag.length > 0);
|
||||||
updateField('tags', tags);
|
updateMetadata('tags', tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|||||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@@ -113,10 +113,10 @@ importers:
|
|||||||
specifier: ^14.5.1
|
specifier: ^14.5.1
|
||||||
version: 14.6.1(@testing-library/dom@10.4.0)
|
version: 14.6.1(@testing-library/dom@10.4.0)
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.23
|
||||||
version: 18.3.23
|
version: 18.3.23
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.7
|
||||||
version: 18.3.7(@types/react@18.3.23)
|
version: 18.3.7(@types/react@18.3.23)
|
||||||
'@types/react-window':
|
'@types/react-window':
|
||||||
specifier: ^1.8.8
|
specifier: ^1.8.8
|
||||||
|
|||||||
Reference in New Issue
Block a user