feat: add complete ComfyUI management system
- Add ComfyUI management page with workflow execution capabilities - Add configuration modal for ComfyUI API settings - Add workflow execution modal with real-time progress tracking - Add workflow publishing modal for sharing workflows - Add ComfyUI service layer for API communication - Add comprehensive TypeScript type definitions - Update navigation to include ComfyUI management - Update Cargo.toml with required dependencies Features: - Real-time workflow execution with WebSocket progress updates - Configurable API settings (URL, timeout, retry, concurrency) - Workflow publishing with metadata management - Error handling and validation - Responsive UI design consistent with existing app
This commit is contained in:
448
apps/desktop/src/pages/ComfyUIManagement.tsx
Normal file
448
apps/desktop/src/pages/ComfyUIManagement.tsx
Normal file
@@ -0,0 +1,448 @@
|
||||
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,
|
||||
ServerStatus,
|
||||
ComfyuiConfig,
|
||||
ComfyuiUIState,
|
||||
ServerConnectionStatus,
|
||||
WorkflowExecutionStatus,
|
||||
PublishWorkflowResponse,
|
||||
ExecuteWorkflowResponse,
|
||||
} from '../types/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>
|
||||
|
||||
<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;
|
||||
Reference in New Issue
Block a user