Files
mixvideo-v2/apps/desktop/src/pages/ComfyUIManagement.tsx
imeepos c1351689cf feat: implement comprehensive workflow management system
- Add workflow creation, editing, and preview components
- Implement execution monitoring and history tracking
- Add batch operations and retry management
- Create environment configuration system
- Add data export and error analysis features
- Update navigation and main app structure
- Enhance Tauri backend integration
- Add comprehensive workflow type definitions
2025-08-07 13:50:35 +08:00

452 lines
17 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.
import React, { useState, useEffect } from 'react';
import {
Play,
Plus,
Settings,
Server,
RefreshCw,
Workflow,
AlertCircle,
CheckCircle,
XCircle,
Clock,
} from 'lucide-react';
import ComfyuiService from '../services/comfyuiService';
import ComfyUIConfigModal from '../components/ComfyUIConfigModal';
import ComfyUIExecuteModal from '../components/ComfyUIExecuteModal';
import ComfyUIPublishModal from '../components/ComfyUIPublishModal';
import type {
Workflow as WorkflowType,
ComfyuiConfig,
ComfyuiUIState,
PublishWorkflowResponse,
ExecuteWorkflowResponse,
} from '../types/comfyui';
/**
* ComfyUI 集群管理页面
* 管理分布式 ComfyUI 集群和工作流调度
* 遵循 Tauri 开发规范和现有 UI 设计模式
*/
const ComfyUIManagement: React.FC = () => {
// ============================================================================
// 状态管理
// ============================================================================
const [state, setState] = useState<ComfyuiUIState>({
workflows: [],
servers: [],
config: {
base_url: '',
timeout: 30,
retry_attempts: 3,
enable_cache: true,
max_concurrency: 8,
},
connectionStatus: 'disconnected',
executionStatus: 'idle',
loading: {
workflows: false,
servers: false,
execution: false,
config: false,
},
});
const [showConfigModal, setShowConfigModal] = useState(false);
const [showExecuteModal, setShowExecuteModal] = useState(false);
const [showPublishModal, setShowPublishModal] = useState(false);
// ============================================================================
// 生命周期和数据加载
// ============================================================================
useEffect(() => {
initializeData();
}, []);
const initializeData = async () => {
await Promise.all([
loadConfig(),
loadWorkflows(),
loadServers(),
testConnection(),
]);
};
const loadConfig = async () => {
setState(prev => ({ ...prev, loading: { ...prev.loading, config: true } }));
try {
const config = await ComfyuiService.getConfig();
setState(prev => ({ ...prev, config, loading: { ...prev.loading, config: false } }));
} catch (error) {
console.error('Failed to load config:', error);
setState(prev => ({
...prev,
error: `加载配置失败: ${error}`,
loading: { ...prev.loading, config: false }
}));
}
};
const loadWorkflows = async () => {
setState(prev => ({ ...prev, loading: { ...prev.loading, workflows: true } }));
try {
const workflows = await ComfyuiService.getAllWorkflows();
setState(prev => ({
...prev,
workflows,
loading: { ...prev.loading, workflows: false },
error: undefined
}));
} catch (error) {
console.error('Failed to load workflows:', error);
setState(prev => ({
...prev,
error: `加载工作流失败: ${error}`,
loading: { ...prev.loading, workflows: false }
}));
}
};
const loadServers = async () => {
setState(prev => ({ ...prev, loading: { ...prev.loading, servers: true } }));
try {
const servers = await ComfyuiService.getServersStatus();
setState(prev => ({
...prev,
servers,
loading: { ...prev.loading, servers: false }
}));
} catch (error) {
console.error('Failed to load servers:', error);
setState(prev => ({
...prev,
error: `加载服务器状态失败: ${error}`,
loading: { ...prev.loading, servers: false }
}));
}
};
const testConnection = async () => {
setState(prev => ({ ...prev, connectionStatus: 'connecting' }));
try {
const isConnected = await ComfyuiService.testConnection();
setState(prev => ({
...prev,
connectionStatus: isConnected ? 'connected' : 'disconnected'
}));
} catch (error) {
console.error('Connection test failed:', error);
setState(prev => ({ ...prev, connectionStatus: 'error' }));
}
};
// ============================================================================
// 事件处理
// ============================================================================
const handleRefresh = () => {
initializeData();
};
const handleExecuteWorkflow = (workflow: WorkflowType) => {
setState(prev => ({ ...prev, selectedWorkflow: workflow }));
setShowExecuteModal(true);
};
const handleDeleteWorkflow = async (workflow: WorkflowType) => {
if (!confirm(`确定要删除工作流 "${workflow.name}" 吗?`)) {
return;
}
try {
await ComfyuiService.deleteWorkflow(workflow.name);
await loadWorkflows(); // 重新加载工作流列表
} catch (error) {
setState(prev => ({ ...prev, error: `删除工作流失败: ${error}` }));
}
};
const handleConfigUpdate = (newConfig: ComfyuiConfig) => {
setState(prev => ({ ...prev, config: newConfig }));
// 配置更新后重新测试连接
testConnection();
};
const handlePublishComplete = (result: PublishWorkflowResponse) => {
if (result.success) {
loadWorkflows(); // 重新加载工作流列表
setShowPublishModal(false);
}
};
const handleExecutionComplete = (result: ExecuteWorkflowResponse) => {
// 执行完成后的处理逻辑
console.log('Workflow execution completed:', result);
};
// ============================================================================
// 渲染辅助函数
// ============================================================================
const getConnectionStatusIcon = () => {
switch (state.connectionStatus) {
case 'connected':
return <CheckCircle className="w-5 h-5 text-green-500" />;
case 'connecting':
return <Clock className="w-5 h-5 text-yellow-500 animate-spin" />;
case 'error':
return <XCircle className="w-5 h-5 text-red-500" />;
default:
return <AlertCircle className="w-5 h-5 text-gray-400" />;
}
};
const getConnectionStatusText = () => {
switch (state.connectionStatus) {
case 'connected':
return '已连接';
case 'connecting':
return '连接中...';
case 'error':
return '连接错误';
default:
return '未连接';
}
};
// ============================================================================
// 主渲染
// ============================================================================
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto py-8 px-4">
{/* 页面标题和操作栏 */}
<div className="bg-white rounded-lg shadow-sm mb-6">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-gray-900 flex items-center gap-2">
<Workflow className="w-6 h-6" />
ComfyUI
</h1>
<p className="text-gray-600 mt-1"> ComfyUI </p>
<div className="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<p className="text-sm text-amber-700">
<strong></strong> ComfyUI
</p>
</div>
</div>
<div className="flex items-center gap-3">
{/* 连接状态 */}
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-gray-100">
{getConnectionStatusIcon()}
<span className="text-sm font-medium">{getConnectionStatusText()}</span>
</div>
{/* 操作按钮 */}
<button
onClick={handleRefresh}
disabled={state.loading.workflows || state.loading.servers}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${state.loading.workflows || state.loading.servers ? 'animate-spin' : ''}`} />
</button>
<button
onClick={() => setShowPublishModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
<Plus className="w-4 h-4" />
</button>
<button
onClick={() => setShowConfigModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors"
>
<Settings className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
{/* 错误提示 */}
{state.error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div className="flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-red-500" />
<span className="text-red-700">{state.error}</span>
<button
onClick={() => setState(prev => ({ ...prev, error: undefined }))}
className="ml-auto text-red-500 hover:text-red-700"
>
<XCircle className="w-4 h-4" />
</button>
</div>
</div>
)}
{/* 主要内容区域 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 工作流列表 */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900"></h2>
</div>
<div className="p-6">
{state.loading.workflows ? (
<div className="flex items-center justify-center py-8">
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
<span className="ml-2 text-gray-500">...</span>
</div>
) : state.workflows.length === 0 ? (
<div className="text-center py-8">
<Workflow className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500"></p>
<button
onClick={() => setShowPublishModal(true)}
className="mt-4 text-blue-600 hover:text-blue-700"
>
</button>
</div>
) : (
<div className="space-y-4">
{state.workflows.map((workflow) => (
<div
key={workflow.name}
className="border border-gray-200 rounded-lg p-4 hover:border-gray-300 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3 className="font-medium text-gray-900">{workflow.name}</h3>
{workflow.description && (
<p className="text-sm text-gray-600 mt-1">{workflow.description}</p>
)}
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
{workflow.version && <span>: {workflow.version}</span>}
{workflow.created_at && (
<span>: {ComfyuiService.formatDateTime(workflow.created_at)}</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleExecuteWorkflow(workflow)}
className="flex items-center gap-1 px-3 py-1 bg-green-100 text-green-700 rounded-md hover:bg-green-200 transition-colors"
>
<Play className="w-3 h-3" />
</button>
<button
onClick={() => handleDeleteWorkflow(workflow)}
className="flex items-center gap-1 px-3 py-1 bg-red-100 text-red-700 rounded-md hover:bg-red-200 transition-colors"
>
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* 服务器状态 */}
<div>
<div className="bg-white rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<Server className="w-5 h-5" />
</h2>
</div>
<div className="p-6">
{state.loading.servers ? (
<div className="flex items-center justify-center py-4">
<RefreshCw className="w-5 h-5 animate-spin text-gray-400" />
<span className="ml-2 text-gray-500">...</span>
</div>
) : state.servers.length === 0 ? (
<div className="text-center py-4">
<Server className="w-8 h-8 text-gray-300 mx-auto mb-2" />
<p className="text-gray-500 text-sm"></p>
</div>
) : (
<div className="space-y-3">
{state.servers.map((server) => (
<div
key={server.server_index}
className="border border-gray-200 rounded-lg p-3"
>
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-sm"> {server.server_index}</span>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
ComfyuiService.getServerStatusColor(server) === 'success'
? 'bg-green-100 text-green-700'
: ComfyuiService.getServerStatusColor(server) === 'warning'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}
>
{ComfyuiService.getServerStatusText(server)}
</span>
</div>
<div className="text-xs text-gray-600 space-y-1">
<div>URL: {server.http_url}</div>
{server.is_reachable && (
<div>
队列: 运行 {server.queue_details.running_count} / {server.queue_details.pending_count}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* 模态框组件 */}
<ComfyUIConfigModal
isOpen={showConfigModal}
onClose={() => setShowConfigModal(false)}
currentConfig={state.config}
onConfigUpdate={handleConfigUpdate}
/>
<ComfyUIExecuteModal
isOpen={showExecuteModal}
onClose={() => setShowExecuteModal(false)}
workflow={state.selectedWorkflow || null}
onExecutionComplete={handleExecutionComplete}
/>
<ComfyUIPublishModal
isOpen={showPublishModal}
onClose={() => setShowPublishModal(false)}
onPublishComplete={handlePublishComplete}
/>
</div>
);
};
export default ComfyUIManagement;