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:
imeepos
2025-08-04 10:42:15 +08:00
parent dccbb7cda6
commit aaabe7942c
7 changed files with 1797 additions and 0 deletions

View 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;