完整实现 Swarm 多智能体协作系统
- 新增 CLIPluginAdapter 统一接口 (backend/app/core/agent_adapter.py) - 新增 LLM 服务层,支持 Anthropic/OpenAI/DeepSeek/Ollama (backend/app/services/llm_service.py) - 新增 Agent 执行引擎,支持文件锁自动管理 (backend/app/services/agent_executor.py) - 新增 NativeLLMAgent 原生 LLM 适配器 (backend/app/adapters/native_llm_agent.py) - 新增进程管理器 (backend/app/services/process_manager.py) - 新增 Agent 控制 API (backend/app/routers/agents_control.py) - 新增 WebSocket 实时通信 (backend/app/routers/websocket.py) - 更新前端 AgentsPage,支持启动/停止 Agent - 测试通过:Agent 启动、批量操作、栅栏同步 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
734
frontend/src/pages/WorkflowPage.tsx
Normal file
734
frontend/src/pages/WorkflowPage.tsx
Normal file
@@ -0,0 +1,734 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Workflow,
|
||||
CheckCircle,
|
||||
FileText,
|
||||
Upload,
|
||||
ChevronRight,
|
||||
RefreshCw,
|
||||
Zap,
|
||||
Users,
|
||||
ArrowRight,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { api } from '../lib/api';
|
||||
import type { Workflow as WorkflowType, WorkflowMeeting } from '../types';
|
||||
|
||||
// 工作流详情面板
|
||||
function WorkflowDetailPanel({
|
||||
workflow,
|
||||
onClose,
|
||||
onReload,
|
||||
}: {
|
||||
workflow: WorkflowType;
|
||||
onClose: () => void;
|
||||
onReload: () => void;
|
||||
}) {
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [executionStatus, setExecutionStatus] = useState<Record<string, any>>({});
|
||||
|
||||
// 刷新执行节点状态
|
||||
const refreshExecutionStatus = async (meetingId: string) => {
|
||||
if (workflow.meetings.find(m => m.meeting_id === meetingId)?.node_type === 'execution') {
|
||||
try {
|
||||
const status = await api.workflow.getExecutionStatus(workflow.workflow_id, meetingId);
|
||||
setExecutionStatus(prev => ({ ...prev, [meetingId]: status }));
|
||||
} catch (err) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 刷新所有执行节点的状态
|
||||
workflow.meetings.forEach(m => {
|
||||
if (m.node_type === 'execution') {
|
||||
refreshExecutionStatus(m.meeting_id);
|
||||
}
|
||||
});
|
||||
}, [workflow]);
|
||||
|
||||
const handleCompleteMeeting = async (meetingId: string) => {
|
||||
setLoading(meetingId);
|
||||
try {
|
||||
await api.workflow.complete(workflow.workflow_id, meetingId);
|
||||
onReload();
|
||||
} catch (err) {
|
||||
alert('标记完成失败');
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFailAndJump = async (meetingId: string) => {
|
||||
const meeting = workflow.meetings.find(m => m.meeting_id === meetingId);
|
||||
if (!meeting?.on_failure) {
|
||||
alert('此节点未配置失败跳转目标');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`确定要跳转到 "${meeting.on_failure}" 吗?`)) {
|
||||
return;
|
||||
}
|
||||
setLoading(`fail-${meetingId}`);
|
||||
try {
|
||||
const result = await api.workflow.handleFailure(workflow.workflow_id, meetingId);
|
||||
if (result.target) {
|
||||
onReload();
|
||||
} else {
|
||||
alert('未配置失败跳转');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('操作失败');
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getMeetingStatus = (meeting: WorkflowMeeting) => {
|
||||
if (meeting.completed) return 'completed';
|
||||
const allPrevCompleted = workflow.meetings
|
||||
.slice(0, workflow.meetings.findIndex((m) => m.meeting_id === meeting.meeting_id))
|
||||
.every((m) => m.completed);
|
||||
return allPrevCompleted ? 'active' : 'pending';
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 480,
|
||||
background: 'rgba(17, 24, 39, 0.98)',
|
||||
borderLeft: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
padding: 24,
|
||||
zIndex: 100,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 20 }}>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#fff', margin: 0 }}>工作流详情</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
fontSize: 24,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Workflow Info */}
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: 12,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<h3 style={{ fontSize: 18, fontWeight: 600, color: '#fff', margin: '0 0 8px 0' }}>
|
||||
{workflow.name}
|
||||
</h3>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: 0 }}>
|
||||
{workflow.workflow_id}
|
||||
</p>
|
||||
<p style={{ fontSize: 14, color: 'rgba(255, 255, 255, 0.6)', margin: '12px 0 0 0' }}>
|
||||
{workflow.description}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
borderRadius: 12,
|
||||
background:
|
||||
workflow.status === 'completed'
|
||||
? '#00ff9d20'
|
||||
: workflow.status === 'in_progress'
|
||||
? '#00f0ff20'
|
||||
: '#ff950020',
|
||||
color:
|
||||
workflow.status === 'completed'
|
||||
? '#00ff9d'
|
||||
: workflow.status === 'in_progress'
|
||||
? '#00f0ff'
|
||||
: '#ff9500',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{workflow.status === 'completed' ? '已完成' : workflow.status === 'in_progress' ? '进行中' : '等待中'}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
borderRadius: 12,
|
||||
background: 'rgba(139, 92, 246, 0.2)',
|
||||
color: '#8b5cf6',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
进度: {workflow.progress}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
height: 8,
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: workflow.progress,
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
|
||||
borderRadius: 4,
|
||||
transition: 'width 0.5s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meetings */}
|
||||
<div>
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, color: 'rgba(255, 255, 255, 0.7)', margin: '0 0 12px 0' }}>
|
||||
流程节点 ({workflow.meetings.length})
|
||||
</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{workflow.meetings.map((meeting, index) => {
|
||||
const status = getMeetingStatus(meeting);
|
||||
const isExecution = meeting.node_type === 'execution';
|
||||
const execStatus = executionStatus[meeting.meeting_id];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={meeting.meeting_id}
|
||||
style={{
|
||||
padding: 16,
|
||||
background:
|
||||
status === 'completed'
|
||||
? 'rgba(0, 255, 157, 0.1)'
|
||||
: status === 'active'
|
||||
? 'rgba(0, 240, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 10,
|
||||
border:
|
||||
status === 'active'
|
||||
? '1px solid rgba(0, 240, 255, 0.3)'
|
||||
: '1px solid transparent',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
{/* 节点图标 */}
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background:
|
||||
status === 'completed'
|
||||
? '#00ff9d'
|
||||
: status === 'active'
|
||||
? isExecution ? '#ff9500' : '#00f0ff'
|
||||
: 'rgba(255, 255, 255, 0.1)',
|
||||
color: status === 'pending' ? 'rgba(255, 255, 255, 0.4)' : '#000',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{status === 'completed' ? <CheckCircle size={14} /> : isExecution ? <Zap size={14} /> : index + 1}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{/* 标题和类型 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<p style={{ fontSize: 14, fontWeight: 500, color: '#fff', margin: 0 }}>
|
||||
{meeting.title}
|
||||
</p>
|
||||
{/* 节点类型标签 */}
|
||||
<span
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
background: isExecution ? '#ff950020' : '#00f0ff20',
|
||||
color: isExecution ? '#ff9500' : '#00f0ff',
|
||||
fontSize: 10,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{isExecution ? '执行' : '会议'}
|
||||
</span>
|
||||
{/* 失败跳转标签 */}
|
||||
{meeting.on_failure && (
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
background: '#ff006e20',
|
||||
color: '#ff006e',
|
||||
fontSize: 10,
|
||||
}}
|
||||
>
|
||||
<AlertTriangle size={10} />
|
||||
失败 → {meeting.on_failure}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ID 和参会者 */}
|
||||
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
|
||||
{meeting.meeting_id}
|
||||
</p>
|
||||
|
||||
{/* 参会者列表 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 6, flexWrap: 'wrap' }}>
|
||||
<Users size={12} color="rgba(255, 255, 255, 0.4)" />
|
||||
{meeting.attendees.map((attendee, i) => (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
>
|
||||
{attendee}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 执行节点进度 */}
|
||||
{isExecution && execStatus && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
进度: {execStatus.progress}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: execStatus.is_ready ? '#00ff9d' : '#ff9500' }}>
|
||||
{execStatus.is_ready ? '就绪' : '进行中'}
|
||||
</span>
|
||||
</div>
|
||||
{execStatus.missing && execStatus.missing.length > 0 && (
|
||||
<div style={{ fontSize: 10, color: 'rgba(255, 255, 255, 0.4)' }}>
|
||||
等待中: {execStatus.missing.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 依赖关系 */}
|
||||
{meeting.depends_on && meeting.depends_on.length > 0 && (
|
||||
<div style={{ marginTop: 6, fontSize: 10, color: 'rgba(255, 255, 255, 0.3)' }}>
|
||||
依赖: {meeting.depends_on.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'flex-end' }}>
|
||||
{status === 'active' && !meeting.completed && (
|
||||
<button
|
||||
onClick={() => handleCompleteMeeting(meeting.meeting_id)}
|
||||
disabled={loading === meeting.meeting_id}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#00ff9d',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
color: '#000',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: loading === meeting.meeting_id ? 'not-allowed' : 'pointer',
|
||||
opacity: loading === meeting.meeting_id ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{loading === meeting.meeting_id ? '...' : '完成'}
|
||||
</button>
|
||||
)}
|
||||
{meeting.completed && meeting.on_failure && (
|
||||
<button
|
||||
onClick={() => handleFailAndJump(meeting.meeting_id)}
|
||||
disabled={loading === `fail-${meeting.meeting_id}`}
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
background: '#ff006e20',
|
||||
border: '1px solid #ff006e50',
|
||||
borderRadius: 6,
|
||||
color: '#ff006e',
|
||||
fontSize: 11,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
触发失败跳转
|
||||
</button>
|
||||
)}
|
||||
{meeting.completed && (
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
background: '#00ff9d20',
|
||||
borderRadius: 12,
|
||||
color: '#00ff9d',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
已完成
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 节点间的连接线 */}
|
||||
{index < workflow.meetings.length - 1 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: 8 }}>
|
||||
<ArrowRight size={16} color="rgba(255, 255, 255, 0.2)" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowPage() {
|
||||
const [workflows, setWorkflows] = useState<WorkflowType[]>([]);
|
||||
const [selectedWorkflow, setSelectedWorkflow] = useState<WorkflowType | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [starting, setStarting] = useState<string | null>(null);
|
||||
|
||||
const loadWorkflows = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 加载已加载的工作流列表
|
||||
const listRes = await api.workflow.list();
|
||||
const loadedWorkflows: WorkflowType[] = [];
|
||||
|
||||
// 从已加载的工作流中获取详情
|
||||
for (const wf of listRes.workflows) {
|
||||
try {
|
||||
const detail = await api.workflow.get(wf.workflow_id);
|
||||
loadedWorkflows.push(detail);
|
||||
} catch {
|
||||
// 忽略加载失败的
|
||||
}
|
||||
}
|
||||
|
||||
setWorkflows(loadedWorkflows);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startDefaultWorkflow = async () => {
|
||||
setStarting('default-dev-flow');
|
||||
try {
|
||||
const workflow = await api.workflow.start('default-dev-flow.yaml');
|
||||
setWorkflows(prev => [...prev.filter(w => w.workflow_id !== workflow.workflow_id), workflow]);
|
||||
setSelectedWorkflow(workflow);
|
||||
} catch (err) {
|
||||
alert('启动默认工作流失败: ' + (err instanceof Error ? err.message : '未知错误'));
|
||||
} finally {
|
||||
setStarting(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkflows();
|
||||
const interval = setInterval(loadWorkflows, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file.name.endsWith('.yaml') && !file.name.endsWith('.yml')) {
|
||||
alert('请上传 YAML 文件');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
await file.text();
|
||||
// 这里应该调用后端 API 上传文件
|
||||
// 暂时模拟成功
|
||||
alert(`文件 ${file.name} 上传成功`);
|
||||
loadWorkflows();
|
||||
} catch (err) {
|
||||
alert('上传失败');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return '#00ff9d';
|
||||
case 'in_progress':
|
||||
return '#00f0ff';
|
||||
default:
|
||||
return '#ff9500';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && workflows.length === 0) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
|
||||
<div style={{ color: '#00f0ff' }}>加载中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<div>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 700, color: '#fff', margin: 0 }}>工作流</h1>
|
||||
<p style={{ fontSize: 14, color: 'rgba(255, 255, 255, 0.5)', margin: '8px 0 0 0' }}>
|
||||
管理和执行工作流 - 包含会议节点和执行节点
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button
|
||||
onClick={loadWorkflows}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '10px 16px',
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 8,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
刷新
|
||||
</button>
|
||||
<button
|
||||
onClick={startDefaultWorkflow}
|
||||
disabled={starting === 'default-dev-flow'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '10px 16px',
|
||||
background: 'rgba(0, 240, 255, 0.1)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.3)',
|
||||
borderRadius: 8,
|
||||
color: '#00f0ff',
|
||||
fontSize: 14,
|
||||
cursor: starting === 'default-dev-flow' ? 'not-allowed' : 'pointer',
|
||||
opacity: starting === 'default-dev-flow' ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<Zap size={16} />
|
||||
{starting === 'default-dev-flow' ? '启动中...' : '启动默认工作流'}
|
||||
</button>
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '10px 16px',
|
||||
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
color: '#000',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
cursor: uploading ? 'not-allowed' : 'pointer',
|
||||
opacity: uploading ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<Upload size={18} />
|
||||
{uploading ? '上传中...' : '上传工作流'}
|
||||
<input type="file" accept=".yaml,.yml" onChange={handleFileUpload} disabled={uploading} style={{ display: 'none' }} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
background: '#ff006e20',
|
||||
border: '1px solid #ff006e50',
|
||||
borderRadius: 8,
|
||||
color: '#ff006e',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
连接后端失败: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Workflows Grid */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.workflow_id}
|
||||
onClick={() => setSelectedWorkflow(workflow)}
|
||||
style={{
|
||||
padding: 20,
|
||||
background: 'rgba(17, 24, 39, 0.7)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(0, 240, 255, 0.3)';
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(0, 240, 255, 0.1)';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 10,
|
||||
background: `linear-gradient(135deg, ${getStatusColor(workflow.status)}40 0%, ${getStatusColor(workflow.status)}10 100%)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Workflow size={24} color={getStatusColor(workflow.status)} />
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: 12,
|
||||
background: `${getStatusColor(workflow.status)}20`,
|
||||
color: getStatusColor(workflow.status),
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
{workflow.status === 'completed' ? '已完成' : workflow.status === 'in_progress' ? '进行中' : '等待中'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: '0 0 4px 0' }}>
|
||||
{workflow.name}
|
||||
</h3>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '0 0 12px 0' }}>
|
||||
{workflow.workflow_id}
|
||||
</p>
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
margin: '0 0 16px 0',
|
||||
lineHeight: 1.5,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{workflow.description}
|
||||
</p>
|
||||
|
||||
{/* Progress */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
height: 6,
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: workflow.progress,
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
|
||||
borderRadius: 3,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<FileText size={14} color="rgba(255, 255, 255, 0.4)" />
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
{workflow.meetings.length} 个节点
|
||||
</span>
|
||||
{/* 统计节点类型 */}
|
||||
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.3)' }}>
|
||||
({workflow.meetings.filter(m => m.node_type === 'execution').length} 执行 + {workflow.meetings.filter(m => m.node_type === 'meeting').length} 会议)
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}>查看详情</span>
|
||||
<ChevronRight size={14} color="rgba(255, 255, 255, 0.4)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{workflows.length === 0 && !loading && (
|
||||
<div style={{ textAlign: 'center', padding: 80 }}>
|
||||
<Workflow size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
|
||||
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}>暂无工作流</p>
|
||||
<p style={{ color: 'rgba(255, 255, 255, 0.3)', fontSize: 14 }}>上传 YAML 文件创建</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Detail Panel */}
|
||||
{selectedWorkflow && (
|
||||
<WorkflowDetailPanel
|
||||
workflow={selectedWorkflow}
|
||||
onClose={() => setSelectedWorkflow(null)}
|
||||
onReload={loadWorkflows}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user