Files
mixvideo-v2/apps/desktop/src/components/comfyui/WorkflowV2Creator.tsx
imeepos b1e7191c10 feat: 添加AI模型面部头发修复工具
- 新增AI模型面部头发修复工具,支持单张图片和批量处理
- 基于ComfyUI的AI_MODEL_FACE_HAIR_FIX_TEMPLATE模板
- 支持自定义面部提示词和去噪强度参数
- 实现实时进度监听和结果展示
- 添加文件选择和路径管理功能
- 修复多个TypeScript编译错误
- 优化UI组件的类型定义和错误处理

新增功能:
- ai_model_face_hair_fix_single_image: 单张图片处理命令
- ai_model_face_hair_fix_batch_images: 批量图片处理命令
- AiModelFaceHairFixTool: 完整的前端UI组件

修复问题:
- ExecutionMonitor组件的showCompleted状态管理
- WorkflowManager的类型注解问题
- WorkflowV2Creator的变量名和状态引用
- Input组件的size属性类型冲突
- comfyuiV2Service缺失的updateTemplate方法
2025-08-11 00:52:21 +08:00

470 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ComfyUI V2 工作流模板创建器组件
* 专门用于创建符合ComfyUI SDK规范的WorkflowTemplateData格式的模板
*/
import React, { useState, useEffect } from 'react';
import {
X,
Save,
Upload,
FileText,
Settings,
Info,
AlertCircle,
CheckCircle,
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 WorkflowV2CreatorProps {
/** 是否显示模态框 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 保存回调 */
onSave: (templateData: CreateTemplateRequest) => Promise<void>;
/** 编辑的模板(为空时表示创建新模板) */
editingTemplate?: any;
}
/**
* ComfyUI V2 工作流创建器组件
*/
export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
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('{}');
const [newParameterName, setNewParameterName] = 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 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 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);
};
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: 'advanced', 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) => setTemplateData(prev => ({
...prev,
metadata: { ...prev.metadata, name: 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="例如:图像生成工作流"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<textarea
value={templateData.metadata.description || ''}
onChange={(e) => setTemplateData(prev => ({
...prev,
metadata: { ...prev.metadata, 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>
<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="用逗号分隔例如AI, 图像, 生成"
/>
<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_json ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="粘贴ComfyUI导出的工作流JSON..."
/>
{errors.workflow_json && (
<p className="mt-1 text-sm text-red-600">{errors.workflow_json}</p>
)}
<p className="mt-1 text-sm text-gray-500">
ComfyUI界面导出工作流JSON并粘贴到这里
</p>
</div>
</div>
)}
{/* 高级设置标签页 - 暂时隐藏 */}
{false && (
<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></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>
);
};