完整实现 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:
32
frontend/src/App.tsx
Normal file
32
frontend/src/App.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
Navigate,
|
||||
} from 'react-router-dom';
|
||||
import { AppLayout } from './components/layout/AppLayout';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { AgentsPage } from './pages/AgentsPage';
|
||||
import { MeetingsPage } from './pages/MeetingsPage';
|
||||
import { ResourcesPage } from './pages/ResourcesPage';
|
||||
import { WorkflowPage } from './pages/WorkflowPage';
|
||||
import { SettingsPage } from './pages/SettingsPage';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <AppLayout />,
|
||||
children: [
|
||||
{ index: true, element: <DashboardPage /> },
|
||||
{ path: 'agents', element: <AgentsPage /> },
|
||||
{ path: 'meetings', element: <MeetingsPage /> },
|
||||
{ path: 'resources', element: <ResourcesPage /> },
|
||||
{ path: 'workflow', element: <WorkflowPage /> },
|
||||
{ path: 'settings', element: <SettingsPage /> },
|
||||
{ path: '*', element: <Navigate to="/" replace /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
230
frontend/src/components/ActionBar.tsx
Normal file
230
frontend/src/components/ActionBar.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { Play, Pause, RotateCcw, Settings, Download, Terminal, Bell, RefreshCw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function ActionBar() {
|
||||
const [running, setRunning] = useState(true);
|
||||
const [notif, setNotif] = useState(3);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(10,15,30,0.85)",
|
||||
border: "1px solid rgba(0,240,255,0.1)",
|
||||
borderRadius: 16,
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
padding: "14px 24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 16,
|
||||
flexWrap: "wrap",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Bottom gradient line */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 1,
|
||||
background: "linear-gradient(90deg,transparent,rgba(0,240,255,0.3),transparent)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Left: Status & Controls */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
{/* System status */}
|
||||
<div
|
||||
style={{
|
||||
padding: "6px 14px",
|
||||
background: running ? "rgba(0,255,157,0.08)" : "rgba(255,149,0,0.08)",
|
||||
border: `1px solid ${running ? "rgba(0,255,157,0.25)" : "rgba(255,149,0,0.25)"}`,
|
||||
borderRadius: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: running ? "#00ff9d" : "#ff9500",
|
||||
color: running ? "#00ff9d" : "#ff9500",
|
||||
}}
|
||||
className={running ? "animate-status-working" : "animate-pulse-glow"}
|
||||
/>
|
||||
<span className="font-mono-code" style={{ fontSize: 11, color: running ? "#00ff9d" : "#ff9500" }}>
|
||||
{running ? "SYSTEM RUNNING" : "SYSTEM PAUSED"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Play/Pause */}
|
||||
<button
|
||||
onClick={() => setRunning(!running)}
|
||||
className="btn-secondary"
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
color: running ? "#ff9500" : "#00ff9d",
|
||||
borderColor: running ? "rgba(255,149,0,0.3)" : "rgba(0,255,157,0.3)",
|
||||
background: running ? "rgba(255,149,0,0.08)" : "rgba(0,255,157,0.08)",
|
||||
}}
|
||||
>
|
||||
{running ? <Pause size={14} /> : <Play size={14} />}
|
||||
{running ? "暂停" : "继续"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn-secondary"
|
||||
style={{ padding: "8px 14px", display: "flex", alignItems: "center", gap: 6 }}
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
重置
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn-secondary"
|
||||
style={{ padding: "8px 14px", display: "flex", alignItems: "center", gap: 6 }}
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
同步状态
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Center: Quick stats */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 20 }}>
|
||||
{[
|
||||
{ label: "任务队列", value: "5", color: "#00f0ff" },
|
||||
{ label: "当前会议", value: "1", color: "#ff9500" },
|
||||
{ label: "锁数量", value: "5", color: "#8b5cf6" },
|
||||
{ label: "错误", value: "0", color: "#00ff9d" },
|
||||
].map(s => (
|
||||
<div key={s.label} style={{ textAlign: "center" }}>
|
||||
<div
|
||||
className="font-orbitron"
|
||||
style={{ fontSize: 16, fontWeight: 700, color: s.color, lineHeight: 1 }}
|
||||
>
|
||||
{s.value}
|
||||
</div>
|
||||
<div className="font-mono-code" style={{ fontSize: 9, color: "#6b7280", marginTop: 2 }}>
|
||||
{s.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right: Actions */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
{/* Notification bell */}
|
||||
<button
|
||||
onClick={() => setNotif(0)}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: 40,
|
||||
height: 40,
|
||||
background: "rgba(0,240,255,0.05)",
|
||||
border: "1px solid rgba(0,240,255,0.15)",
|
||||
borderRadius: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#9ca3af",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<Bell size={16} />
|
||||
{notif > 0 && (
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 6,
|
||||
right: 6,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: "50%",
|
||||
background: "#ff006e",
|
||||
fontSize: 8,
|
||||
color: "#fff",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{notif}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
background: "rgba(0,240,255,0.05)",
|
||||
border: "1px solid rgba(0,240,255,0.15)",
|
||||
borderRadius: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#9ca3af",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<Terminal size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
background: "rgba(0,240,255,0.05)",
|
||||
border: "1px solid rgba(0,240,255,0.15)",
|
||||
borderRadius: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#9ca3af",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<Download size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
background: "rgba(0,240,255,0.05)",
|
||||
border: "1px solid rgba(0,240,255,0.15)",
|
||||
borderRadius: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#9ca3af",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<Settings size={16} />
|
||||
</button>
|
||||
|
||||
<button className="btn-primary" style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Play size={13} />
|
||||
启动新任务
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
255
frontend/src/components/AgentStatusCard.tsx
Normal file
255
frontend/src/components/AgentStatusCard.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import { Cpu, Zap, MoreHorizontal } from "lucide-react";
|
||||
|
||||
type AgentStatus = "working" | "waiting" | "idle";
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
fullName: string;
|
||||
role: string;
|
||||
status: AgentStatus;
|
||||
task: string;
|
||||
progress: number;
|
||||
model: string;
|
||||
tokens: number;
|
||||
gradient: string;
|
||||
}
|
||||
|
||||
const agents: Agent[] = [
|
||||
{
|
||||
id: "claude-001",
|
||||
name: "CLA",
|
||||
fullName: "Claude Code",
|
||||
role: "架构师",
|
||||
status: "working",
|
||||
task: "重构认证模块 src/auth/",
|
||||
progress: 68,
|
||||
model: "claude-opus-4.6",
|
||||
tokens: 14203,
|
||||
gradient: "linear-gradient(135deg, #8b5cf6, #6366f1)",
|
||||
},
|
||||
{
|
||||
id: "kimi-002",
|
||||
name: "KIM",
|
||||
fullName: "Kimi CLI",
|
||||
role: "产品经理",
|
||||
status: "waiting",
|
||||
task: "等待需求评审会议",
|
||||
progress: 100,
|
||||
model: "moonshot-v1-8k",
|
||||
tokens: 8921,
|
||||
gradient: "linear-gradient(135deg, #f59e0b, #d97706)",
|
||||
},
|
||||
{
|
||||
id: "opencode-003",
|
||||
name: "OPC",
|
||||
fullName: "OpenCode",
|
||||
role: "开发者",
|
||||
status: "working",
|
||||
task: "生成 API 单元测试用例",
|
||||
progress: 34,
|
||||
model: "gpt-4o",
|
||||
tokens: 6744,
|
||||
gradient: "linear-gradient(135deg, #10b981, #059669)",
|
||||
},
|
||||
{
|
||||
id: "human-001",
|
||||
name: "USR",
|
||||
fullName: "Tech Lead",
|
||||
role: "技术负责人",
|
||||
status: "idle",
|
||||
task: "等待 Agent 完成评审",
|
||||
progress: 0,
|
||||
model: "human",
|
||||
tokens: 0,
|
||||
gradient: "linear-gradient(135deg, #f59e0b, #d97706)",
|
||||
},
|
||||
];
|
||||
|
||||
const statusConfig = {
|
||||
working: { color: "#00ff9d", bg: "rgba(0,255,157,0.1)", label: "Working", border: "rgba(0,255,157,0.4)" },
|
||||
waiting: { color: "#ff9500", bg: "rgba(255,149,0,0.1)", label: "Meeting", border: "rgba(255,149,0,0.4)" },
|
||||
idle: { color: "#6b7280", bg: "rgba(107,114,128,0.1)", label: "Idle", border: "rgba(107,114,128,0.2)" },
|
||||
};
|
||||
|
||||
function AgentItem({ agent }: { agent: Agent }) {
|
||||
const cfg = statusConfig[agent.status];
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(0,0,0,0.3)",
|
||||
border: `1px solid ${cfg.border}`,
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
className={
|
||||
agent.status === "working"
|
||||
? "agent-working"
|
||||
: agent.status === "waiting"
|
||||
? "agent-waiting"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
|
||||
{/* Avatar */}
|
||||
<div
|
||||
className="agent-avatar"
|
||||
style={{ background: agent.gradient, flexShrink: 0 }}
|
||||
>
|
||||
{agent.name}
|
||||
</div>
|
||||
|
||||
{/* Name & Role */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span
|
||||
className="font-orbitron"
|
||||
style={{ fontSize: 12, color: "#e5e7eb", fontWeight: 600 }}
|
||||
>
|
||||
{agent.fullName}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 10, color: "#6b7280", marginTop: 1 }}
|
||||
>
|
||||
{agent.role} · {agent.model}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status badge */}
|
||||
<div
|
||||
className="badge"
|
||||
style={{
|
||||
color: cfg.color,
|
||||
borderColor: `${cfg.color}40`,
|
||||
background: cfg.bg,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{ display: "inline-flex", alignItems: "center", gap: 4 }}
|
||||
>
|
||||
<span
|
||||
style={{ width: 5, height: 5, borderRadius: "50%", background: cfg.color }}
|
||||
className={agent.status !== "idle" ? "animate-pulse-glow" : ""}
|
||||
/>
|
||||
{cfg.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task */}
|
||||
<div
|
||||
className="font-rajdhani"
|
||||
style={{ fontSize: 12, color: "#9ca3af", marginBottom: 8, lineHeight: 1.4 }}
|
||||
>
|
||||
{agent.task}
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
{agent.status !== "idle" && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
|
||||
进度
|
||||
</span>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: cfg.color }}>
|
||||
{agent.progress}%
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 4,
|
||||
background: "#111827",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${agent.progress}%`,
|
||||
background:
|
||||
agent.status === "working"
|
||||
? "linear-gradient(90deg,#00f0ff,#00ff9d)"
|
||||
: "linear-gradient(90deg,#00f0ff,#ff9500)",
|
||||
borderRadius: 2,
|
||||
transition: "width 0.5s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tokens */}
|
||||
{agent.tokens > 0 && (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 8 }}
|
||||
>
|
||||
<Zap size={10} color="#6b7280" />
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
|
||||
{agent.tokens.toLocaleString()} tokens
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentStatusCard() {
|
||||
return (
|
||||
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,240,255,0.1)",
|
||||
border: "1px solid rgba(0,240,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Cpu size={14} color="#00f0ff" />
|
||||
</div>
|
||||
<span className="card-title">Agent 状态</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#00ff9d" }}>
|
||||
● 3 / 4 活跃
|
||||
</span>
|
||||
<button
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "#6b7280",
|
||||
cursor: "pointer",
|
||||
padding: 4,
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent list */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10, flex: 1, overflowY: "auto" }}>
|
||||
{agents.map(agent => (
|
||||
<AgentItem key={agent.id} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
279
frontend/src/components/BarrierSyncCard.tsx
Normal file
279
frontend/src/components/BarrierSyncCard.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import { Shield, Zap } from "lucide-react";
|
||||
|
||||
type NodeState = "ready" | "waiting" | "pending";
|
||||
|
||||
interface BarrierNode {
|
||||
id: string;
|
||||
name: string;
|
||||
gradient: string;
|
||||
state: NodeState;
|
||||
waitingFor?: string;
|
||||
}
|
||||
|
||||
const nodes: BarrierNode[] = [
|
||||
{
|
||||
id: "n1",
|
||||
name: "CLA",
|
||||
gradient: "linear-gradient(135deg,#8b5cf6,#6366f1)",
|
||||
state: "ready",
|
||||
},
|
||||
{
|
||||
id: "n2",
|
||||
name: "KIM",
|
||||
gradient: "linear-gradient(135deg,#f59e0b,#d97706)",
|
||||
state: "waiting",
|
||||
waitingFor: "架构设计完成",
|
||||
},
|
||||
{
|
||||
id: "n3",
|
||||
name: "OPC",
|
||||
gradient: "linear-gradient(135deg,#10b981,#059669)",
|
||||
state: "ready",
|
||||
},
|
||||
{
|
||||
id: "n4",
|
||||
name: "USR",
|
||||
gradient: "linear-gradient(135deg,#f59e0b,#b45309)",
|
||||
state: "pending",
|
||||
},
|
||||
];
|
||||
|
||||
const stateColors: Record<NodeState, string> = {
|
||||
ready: "#00ff9d",
|
||||
waiting: "#ff9500",
|
||||
pending: "#374151",
|
||||
};
|
||||
|
||||
const stateLabels: Record<NodeState, string> = {
|
||||
ready: "READY",
|
||||
waiting: "WAIT",
|
||||
pending: "IDLE",
|
||||
};
|
||||
|
||||
const syncPoints = [
|
||||
{ name: "INIT", completed: true },
|
||||
{ name: "REVIEW", completed: true },
|
||||
{ name: "DESIGN", completed: false, active: true },
|
||||
{ name: "IMPL", completed: false },
|
||||
{ name: "DEPLOY", completed: false },
|
||||
];
|
||||
|
||||
export function BarrierSyncCard() {
|
||||
const readyCount = nodes.filter(n => n.state === "ready").length;
|
||||
|
||||
return (
|
||||
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,240,255,0.1)",
|
||||
border: "1px solid rgba(0,240,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Shield size={14} color="#00f0ff" />
|
||||
</div>
|
||||
<span className="card-title">栅栏同步</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span className="font-mono-code" style={{ fontSize: 11, color: "#ff9500" }}>
|
||||
{readyCount}/{nodes.length} 就绪
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
padding: "3px 10px",
|
||||
background: "rgba(255,149,0,0.1)",
|
||||
border: "1px solid rgba(255,149,0,0.3)",
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#ff9500" }}>
|
||||
<span className="animate-pulse-fast">●</span> 等待触发
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 20, flex: 1, minHeight: 0 }}>
|
||||
{/* Left: Agent nodes */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8, minWidth: 200 }}>
|
||||
{nodes.map(node => (
|
||||
<div
|
||||
key={node.id}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
padding: "8px 12px",
|
||||
background: "rgba(0,0,0,0.3)",
|
||||
border: `1px solid ${stateColors[node.state]}30`,
|
||||
borderRadius: 10,
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="agent-avatar"
|
||||
style={{ background: node.gradient, width: 32, height: 32, fontSize: 10 }}
|
||||
>
|
||||
{node.name}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="font-mono-code" style={{ fontSize: 11, color: "#9ca3af" }}>
|
||||
{node.name === "CLA" ? "Claude Code" : node.name === "KIM" ? "Kimi CLI" : node.name === "OPC" ? "OpenCode" : "Tech Lead"}
|
||||
</div>
|
||||
{node.waitingFor && (
|
||||
<div className="font-mono-code" style={{ fontSize: 9, color: "#ff9500", marginTop: 1 }}>
|
||||
等待: {node.waitingFor}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="barrier-node"
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderColor: stateColors[node.state],
|
||||
color: stateColors[node.state],
|
||||
background: `${stateColors[node.state]}10`,
|
||||
fontSize: 8,
|
||||
}}
|
||||
title={stateLabels[node.state]}
|
||||
>
|
||||
{node.state === "ready" ? (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<path d="M2 6L5 9L10 3" stroke="#00ff9d" strokeWidth="1.5" strokeLinecap="round" fill="none" />
|
||||
</svg>
|
||||
) : node.state === "waiting" ? (
|
||||
<Zap size={12} />
|
||||
) : (
|
||||
<span style={{ fontSize: 8 }}>—</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right: Sync points */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280", marginBottom: 10 }}>
|
||||
同步检查点
|
||||
</div>
|
||||
|
||||
{/* Progress line */}
|
||||
<div style={{ position: "relative", marginBottom: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
height: 3,
|
||||
background: "#111827",
|
||||
borderRadius: 2,
|
||||
position: "relative",
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
{/* Completed portion */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: "100%",
|
||||
width: "42%",
|
||||
background: "linear-gradient(90deg,#00f0ff,#00ff9d)",
|
||||
borderRadius: 2,
|
||||
}}
|
||||
/>
|
||||
{/* Active flowing portion */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "42%",
|
||||
top: 0,
|
||||
height: "100%",
|
||||
width: "16%",
|
||||
background: "linear-gradient(90deg,#ff9500,rgba(255,149,0,0.3))",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: "linear-gradient(90deg,transparent,rgba(255,255,255,0.3),transparent)",
|
||||
animation: "line-flow 1.5s linear infinite",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dots */}
|
||||
{syncPoints.map((sp, i) => {
|
||||
const left = `${(i / (syncPoints.length - 1)) * 100}%`;
|
||||
return (
|
||||
<div
|
||||
key={sp.name}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left,
|
||||
transform: "translate(-50%,-50%)",
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: "50%",
|
||||
border: `2px solid ${sp.completed ? "#00ff9d" : sp.active ? "#ff9500" : "#374151"}`,
|
||||
background: sp.completed ? "#00ff9d" : sp.active ? "#ff9500" : "#111827",
|
||||
zIndex: 2,
|
||||
boxShadow: sp.active ? "0 0 8px rgba(255,149,0,0.6)" : sp.completed ? "0 0 6px rgba(0,255,157,0.4)" : "none",
|
||||
}}
|
||||
className={sp.active ? "animate-scale-pulse" : ""}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 12 }}>
|
||||
{syncPoints.map(sp => (
|
||||
<div
|
||||
key={sp.name}
|
||||
className="font-mono-code"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
color: sp.completed ? "#00ff9d" : sp.active ? "#ff9500" : "#374151",
|
||||
textAlign: "center",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{sp.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status panel */}
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 12px",
|
||||
background: "rgba(255,149,0,0.05)",
|
||||
border: "1px solid rgba(255,149,0,0.15)",
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#ff9500", marginBottom: 4 }}>
|
||||
⏸ 等待 DESIGN 检查点同步
|
||||
</div>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
|
||||
触发条件:所有 Agent 调用 wait_for_meeting("design_review") · 已就绪 {readyCount}/{nodes.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
196
frontend/src/components/ConsensusCard.tsx
Normal file
196
frontend/src/components/ConsensusCard.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { TrendingUp } from "lucide-react";
|
||||
|
||||
const agentVotes = [
|
||||
{ name: "Claude Code", agree: true, weight: 1.5, comment: "Session+Redis 方案合理" },
|
||||
{ name: "Kimi CLI", agree: true, weight: 1.0, comment: "支持简化方案" },
|
||||
{ name: "OpenCode", agree: true, weight: 1.2, comment: "符合项目需求" },
|
||||
{ name: "Tech Lead", agree: true, weight: 2.0, comment: "简单优先原则" },
|
||||
];
|
||||
|
||||
interface ConsensusRingProps {
|
||||
value: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
function ConsensusRing({ value, size = 100 }: ConsensusRingProps) {
|
||||
const r = (size - 8) / 2;
|
||||
const circ = 2 * Math.PI * r;
|
||||
const offset = circ - (value / 100) * circ;
|
||||
|
||||
return (
|
||||
<svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}>
|
||||
{/* Track */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={r}
|
||||
fill="none"
|
||||
stroke="#111827"
|
||||
strokeWidth={4}
|
||||
/>
|
||||
{/* Progress */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={r}
|
||||
fill="none"
|
||||
stroke="#00f0ff"
|
||||
strokeWidth={4}
|
||||
strokeDasharray={circ}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
style={{ transition: "stroke-dashoffset 1s ease", filter: "drop-shadow(0 0 6px rgba(0,240,255,0.5))" }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConsensusCard() {
|
||||
const pct = 78;
|
||||
const totalWeight = agentVotes.reduce((s, a) => s + a.weight, 0);
|
||||
const agreeWeight = agentVotes.filter(a => a.agree).reduce((s, a) => s + a.weight, 0);
|
||||
|
||||
return (
|
||||
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14, flexShrink: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,240,255,0.1)",
|
||||
border: "1px solid rgba(0,240,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<TrendingUp size={14} color="#00f0ff" />
|
||||
</div>
|
||||
<span className="card-title">共识状态</span>
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
color: "#00ff9d",
|
||||
borderColor: "rgba(0,255,157,0.3)",
|
||||
background: "rgba(0,255,157,0.1)",
|
||||
}}
|
||||
>
|
||||
✓ 已达成
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 20, flex: 1 }}>
|
||||
{/* Ring */}
|
||||
<div style={{ position: "relative", flexShrink: 0 }}>
|
||||
<ConsensusRing value={pct} size={96} />
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-orbitron"
|
||||
style={{ fontSize: 20, fontWeight: 700, color: "#00f0ff", lineHeight: 1 }}
|
||||
>
|
||||
{pct}%
|
||||
</span>
|
||||
<span
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 8, color: "#6b7280", marginTop: 2 }}
|
||||
>
|
||||
认同率
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent votes */}
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{agentVotes.map(v => (
|
||||
<div
|
||||
key={v.name}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: "50%",
|
||||
background: v.agree ? "rgba(0,255,157,0.2)" : "rgba(255,0,110,0.2)",
|
||||
border: `1px solid ${v.agree ? "#00ff9d" : "#ff006e"}`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 9, color: v.agree ? "#00ff9d" : "#ff006e" }}>
|
||||
{v.agree ? "✓" : "✗"}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 10, color: "#9ca3af" }}
|
||||
>
|
||||
{v.name}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono-code"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
color: "#6b7280",
|
||||
background: "rgba(0,0,0,0.3)",
|
||||
padding: "1px 4px",
|
||||
borderRadius: 3,
|
||||
}}
|
||||
>
|
||||
w:{v.weight}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Weight bar */}
|
||||
<div style={{ width: 40, height: 3, background: "#111827", borderRadius: 2, overflow: "hidden" }}>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${(v.weight / 2) * 100}%`,
|
||||
background: v.agree ? "#00ff9d" : "#ff006e",
|
||||
borderRadius: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: "8px 12px",
|
||||
background: "rgba(0,255,157,0.05)",
|
||||
border: "1px solid rgba(0,255,157,0.15)",
|
||||
borderRadius: 10,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
|
||||
加权认同 {agreeWeight.toFixed(1)} / {totalWeight.toFixed(1)} · 迭代收敛
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
285
frontend/src/components/DiscussionCard.tsx
Normal file
285
frontend/src/components/DiscussionCard.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import { useState } from "react";
|
||||
import { MessageSquare, CheckCircle, Send } from "lucide-react";
|
||||
|
||||
type MessageType = "proposal" | "comment" | "consensus" | "human";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
agent: string;
|
||||
agentFull: string;
|
||||
gradient: string;
|
||||
type: MessageType;
|
||||
content: string;
|
||||
time: string;
|
||||
round?: number;
|
||||
}
|
||||
|
||||
const initMessages: Message[] = [
|
||||
{
|
||||
id: "m1",
|
||||
agent: "CLA",
|
||||
agentFull: "Claude Code",
|
||||
gradient: "linear-gradient(135deg,#8b5cf6,#6366f1)",
|
||||
type: "proposal",
|
||||
content: "建议使用 JWT + Refresh Token 方案。优点:无状态、易于水平扩展,支持跨域。实现方案:Access Token 15分钟过期,Refresh Token 7天过期。",
|
||||
time: "14:20:03",
|
||||
round: 1,
|
||||
},
|
||||
{
|
||||
id: "m2",
|
||||
agent: "KIM",
|
||||
agentFull: "Kimi CLI",
|
||||
gradient: "linear-gradient(135deg,#f59e0b,#d97706)",
|
||||
type: "comment",
|
||||
content: "需要考虑 Token 撤销问题。JWT 无状态特性导致主动撤销困难,建议结合 Redis 黑名单机制,在注销时将 Token 加入黑名单。",
|
||||
time: "14:21:17",
|
||||
round: 1,
|
||||
},
|
||||
{
|
||||
id: "m3",
|
||||
agent: "USR",
|
||||
agentFull: "Tech Lead [HUMAN]",
|
||||
gradient: "linear-gradient(135deg,#f59e0b,#b45309)",
|
||||
type: "human",
|
||||
content: "⚠️ 我们的数据量不大,SQLite + Session 就够了。不要过度设计,保持简单。",
|
||||
time: "14:22:05",
|
||||
},
|
||||
{
|
||||
id: "m4",
|
||||
agent: "OPC",
|
||||
agentFull: "OpenCode",
|
||||
gradient: "linear-gradient(135deg,#10b981,#059669)",
|
||||
type: "comment",
|
||||
content: "收到 Tech Lead 反馈。重新评估方案:Session + Redis 的确更简单,且符合当前规模。支持调整为 Session 方案。",
|
||||
time: "14:23:31",
|
||||
round: 2,
|
||||
},
|
||||
{
|
||||
id: "m5",
|
||||
agent: "CLA",
|
||||
agentFull: "Claude Code",
|
||||
gradient: "linear-gradient(135deg,#8b5cf6,#6366f1)",
|
||||
type: "consensus",
|
||||
content: "✅ 共识达成:采用 Session + Redis 方案。简单可靠,符合当前项目规模。后续如有扩展需求可迁移至 JWT。",
|
||||
time: "14:25:44",
|
||||
round: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const typeConfig = {
|
||||
proposal: { border: "#00f0ff", label: "提案", bg: "rgba(0,240,255,0.05)" },
|
||||
comment: { border: "#ff9500", label: "评论", bg: "rgba(255,149,0,0.05)" },
|
||||
consensus: { border: "#00ff9d", label: "共识", bg: "linear-gradient(135deg,rgba(0,255,157,0.1),rgba(0,240,255,0.1))" },
|
||||
human: { border: "#f59e0b", label: "人类", bg: "rgba(245,158,11,0.05)" },
|
||||
};
|
||||
|
||||
export function DiscussionCard() {
|
||||
const [messages, setMessages] = useState(initMessages);
|
||||
const [filter, setFilter] = useState<MessageType | "all">("all");
|
||||
const [newMsg, setNewMsg] = useState("");
|
||||
|
||||
const filtered = filter === "all" ? messages : messages.filter(m => m.type === filter);
|
||||
|
||||
const sendMsg = () => {
|
||||
if (!newMsg.trim()) return;
|
||||
const m: Message = {
|
||||
id: `m${Date.now()}`,
|
||||
agent: "USR",
|
||||
agentFull: "Tech Lead [HUMAN]",
|
||||
gradient: "linear-gradient(135deg,#f59e0b,#b45309)",
|
||||
type: "human",
|
||||
content: newMsg,
|
||||
time: new Date().toLocaleTimeString("zh-CN", { hour12: false }),
|
||||
};
|
||||
setMessages(prev => [...prev, m]);
|
||||
setNewMsg("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,240,255,0.1)",
|
||||
border: "1px solid rgba(0,240,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<MessageSquare size={14} color="#00f0ff" />
|
||||
</div>
|
||||
<span className="card-title">协作讨论</span>
|
||||
<span
|
||||
className="badge"
|
||||
style={{ color: "#00f0ff", borderColor: "rgba(0,240,255,0.3)", background: "rgba(0,240,255,0.1)" }}
|
||||
>
|
||||
第 2 轮
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 4 }}>
|
||||
{(["all", "proposal", "comment", "consensus", "human"] as const).map(f => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className="font-mono-code"
|
||||
style={{
|
||||
padding: "3px 8px",
|
||||
fontSize: 10,
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${filter === f ? "rgba(0,240,255,0.4)" : "rgba(0,240,255,0.1)"}`,
|
||||
background: filter === f ? "rgba(0,240,255,0.1)" : "transparent",
|
||||
color: filter === f ? "#00f0ff" : "#6b7280",
|
||||
cursor: "pointer",
|
||||
textTransform: "uppercase",
|
||||
transition: "all 0.2s",
|
||||
}}
|
||||
>
|
||||
{f === "all" ? "全部" : f === "proposal" ? "提案" : f === "comment" ? "评论" : f === "consensus" ? "共识" : "人类"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Consensus indicator */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "8px 12px",
|
||||
background: "rgba(0,255,157,0.05)",
|
||||
border: "1px solid rgba(0,255,157,0.2)",
|
||||
borderRadius: 10,
|
||||
marginBottom: 12,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<CheckCircle size={13} color="#00ff9d" />
|
||||
<span className="font-mono-code" style={{ fontSize: 11, color: "#00ff9d" }}>
|
||||
本轮共识已达成 · 78% 认同率 · 迭代次数 2/5
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 10, paddingRight: 4 }}>
|
||||
{filtered.map((msg, i) => {
|
||||
const cfg = typeConfig[msg.type];
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className="animate-message-slide"
|
||||
style={{
|
||||
animationDelay: `${i * 0.05}s`,
|
||||
display: "flex",
|
||||
gap: 10,
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div
|
||||
className="agent-avatar"
|
||||
style={{ background: msg.gradient, flexShrink: 0, marginTop: 2 }}
|
||||
>
|
||||
{msg.agent}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
||||
<span
|
||||
className="font-orbitron"
|
||||
style={{ fontSize: 11, color: "#e5e7eb", fontWeight: 600 }}
|
||||
>
|
||||
{msg.agentFull}
|
||||
</span>
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
color: cfg.border,
|
||||
borderColor: `${cfg.border}40`,
|
||||
background: `${cfg.border}15`,
|
||||
fontSize: 9,
|
||||
}}
|
||||
>
|
||||
{cfg.label}
|
||||
{msg.round ? ` · R${msg.round}` : ""}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 10, color: "#4b5563", marginLeft: "auto" }}
|
||||
>
|
||||
{msg.time}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="font-rajdhani"
|
||||
style={{
|
||||
background: msg.type === "consensus"
|
||||
? "linear-gradient(135deg,rgba(0,255,157,0.1),rgba(0,240,255,0.08))"
|
||||
: "rgba(0,0,0,0.3)",
|
||||
borderLeft: `3px solid ${cfg.border}`,
|
||||
borderRadius: "0 10px 10px 0",
|
||||
padding: "10px 14px",
|
||||
fontSize: 13,
|
||||
color: "#d1d5db",
|
||||
lineHeight: 1.5,
|
||||
border: msg.type === "consensus" ? `1px solid rgba(0,255,157,0.2)` : undefined,
|
||||
borderLeftColor: cfg.border,
|
||||
borderLeftWidth: 3,
|
||||
}}
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
padding: "10px 12px",
|
||||
background: "rgba(0,0,0,0.3)",
|
||||
border: "1px solid rgba(0,240,255,0.1)",
|
||||
borderRadius: 10,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="agent-avatar"
|
||||
style={{ background: "linear-gradient(135deg,#f59e0b,#d97706)", flexShrink: 0 }}
|
||||
>
|
||||
USR
|
||||
</div>
|
||||
<input
|
||||
value={newMsg}
|
||||
onChange={e => setNewMsg(e.target.value)}
|
||||
onKeyDown={e => e.key === "Enter" && sendMsg()}
|
||||
placeholder="参与讨论... (Enter 发送)"
|
||||
style={{
|
||||
flex: 1,
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
color: "#e5e7eb",
|
||||
fontSize: 13,
|
||||
fontFamily: "'Noto Sans SC',sans-serif",
|
||||
}}
|
||||
/>
|
||||
<button onClick={sendMsg} style={{ background: "none", border: "none", cursor: "pointer", color: "#00f0ff", padding: 4 }}>
|
||||
<Send size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
198
frontend/src/components/Header.tsx
Normal file
198
frontend/src/components/Header.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Activity, Wifi, Clock, Zap } from "lucide-react";
|
||||
|
||||
const agents = [
|
||||
{ id: "claude-001", name: "Claude", status: "working" as const },
|
||||
{ id: "kimi-002", name: "Kimi", status: "waiting" as const },
|
||||
{ id: "opencode-003", name: "OpenCode", status: "working" as const },
|
||||
{ id: "human-001", name: "Human", status: "idle" as const },
|
||||
];
|
||||
|
||||
const statusColors = {
|
||||
working: "#00ff9d",
|
||||
waiting: "#ff9500",
|
||||
idle: "#6b7280",
|
||||
};
|
||||
|
||||
export function Header() {
|
||||
const [time, setTime] = useState(new Date());
|
||||
const [uptime, setUptime] = useState(7432);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => {
|
||||
setTime(new Date());
|
||||
setUptime(prev => prev + 1);
|
||||
}, 1000);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
const formatTime = (d: Date) =>
|
||||
d.toLocaleTimeString("zh-CN", { hour12: false });
|
||||
|
||||
const formatUptime = (s: number) => {
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<header
|
||||
style={{
|
||||
background: "rgba(10,15,30,0.85)",
|
||||
borderBottom: "1px solid rgba(0,240,255,0.1)",
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 1800,
|
||||
margin: "0 auto",
|
||||
padding: "0 30px",
|
||||
height: 72,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 20,
|
||||
}}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16, flexShrink: 0 }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
{/* Rotating halo */}
|
||||
<div
|
||||
className="animate-rotate-logo"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: -3,
|
||||
borderRadius: 14,
|
||||
background: "conic-gradient(from 0deg, #00f0ff, #8b5cf6, transparent, #00f0ff)",
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
{/* Logo box */}
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
background: "linear-gradient(135deg, #00f0ff, #8b5cf6)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-orbitron"
|
||||
style={{ fontSize: 22, fontWeight: 900, color: "#030712" }}
|
||||
>
|
||||
S
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="font-orbitron"
|
||||
style={{
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 4,
|
||||
background: "linear-gradient(90deg, #00f0ff, #8b5cf6)",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
backgroundClip: "text",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
SWARM
|
||||
</div>
|
||||
<div
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 10, color: "#6b7280", letterSpacing: 2, marginTop: -2 }}
|
||||
>
|
||||
MULTI-AGENT COMMAND CENTER
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Status Pills */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||
{agents.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
background: "rgba(0,0,0,0.3)",
|
||||
border: `1px solid ${statusColors[agent.status]}33`,
|
||||
borderRadius: 20,
|
||||
padding: "5px 12px",
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
background: statusColors[agent.status],
|
||||
color: statusColors[agent.status],
|
||||
flexShrink: 0,
|
||||
}}
|
||||
className={agent.status !== "idle" ? "animate-pulse-glow" : ""}
|
||||
/>
|
||||
<span
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 11, color: statusColors[agent.status], fontWeight: 500 }}
|
||||
>
|
||||
{agent.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* System Stats */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 20, flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Wifi size={14} color="#6b7280" />
|
||||
<div>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>LATENCY</div>
|
||||
<div className="font-mono-code" style={{ fontSize: 12, color: "#00ff9d" }}>42ms</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: 1, height: 32, background: "rgba(0,240,255,0.1)" }} />
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Activity size={14} color="#6b7280" />
|
||||
<div>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>UPTIME</div>
|
||||
<div className="font-mono-code" style={{ fontSize: 12, color: "#00f0ff" }}>{formatUptime(uptime)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: 1, height: 32, background: "rgba(0,240,255,0.1)" }} />
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Zap size={14} color="#6b7280" />
|
||||
<div>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>ACTIVE</div>
|
||||
<div className="font-mono-code" style={{ fontSize: 12, color: "#ff9500" }}>3 / 4</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: 1, height: 32, background: "rgba(0,240,255,0.1)" }} />
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Clock size={14} color="#6b7280" />
|
||||
<div>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>LOCAL TIME</div>
|
||||
<div className="font-mono-code" style={{ fontSize: 12, color: "#e5e7eb" }}>{formatTime(time)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
228
frontend/src/components/MeetingProgressCard.tsx
Normal file
228
frontend/src/components/MeetingProgressCard.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { Users, Clock } from "lucide-react";
|
||||
|
||||
type NodeStatus = "completed" | "active" | "pending";
|
||||
|
||||
interface MeetingStep {
|
||||
id: string;
|
||||
label: string;
|
||||
status: NodeStatus;
|
||||
time?: string;
|
||||
}
|
||||
|
||||
const steps: MeetingStep[] = [
|
||||
{ id: "s1", label: "收集初步想法", status: "completed", time: "14:10" },
|
||||
{ id: "s2", label: "讨论与迭代", status: "active", time: "14:18" },
|
||||
{ id: "s3", label: "生成共识版本", status: "pending" },
|
||||
{ id: "s4", label: "记录会议文件", status: "pending" },
|
||||
];
|
||||
|
||||
const statusColors: Record<NodeStatus, string> = {
|
||||
completed: "#00ff9d",
|
||||
active: "#ff9500",
|
||||
pending: "#374151",
|
||||
};
|
||||
|
||||
const attendees = [
|
||||
{ name: "CLA", gradient: "linear-gradient(135deg,#8b5cf6,#6366f1)" },
|
||||
{ name: "KIM", gradient: "linear-gradient(135deg,#f59e0b,#d97706)" },
|
||||
{ name: "OPC", gradient: "linear-gradient(135deg,#10b981,#059669)" },
|
||||
{ name: "USR", gradient: "linear-gradient(135deg,#f59e0b,#b45309)" },
|
||||
];
|
||||
|
||||
export function MeetingProgressCard() {
|
||||
return (
|
||||
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
background: "rgba(255,149,0,0.1)",
|
||||
border: "1px solid rgba(255,149,0,0.3)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Users size={14} color="#ff9500" />
|
||||
</div>
|
||||
<span className="card-title">会议进度</span>
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
color: "#ff9500",
|
||||
borderColor: "rgba(255,149,0,0.4)",
|
||||
background: "rgba(255,149,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<span className="animate-pulse-fast" style={{ color: "#ff9500" }}>● </span>
|
||||
进行中
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<Clock size={12} color="#6b7280" />
|
||||
<span className="font-mono-code" style={{ fontSize: 11, color: "#ff9500" }}>
|
||||
08:23
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meeting name */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
background: "rgba(255,149,0,0.05)",
|
||||
border: "1px solid rgba(255,149,0,0.2)",
|
||||
borderRadius: 10,
|
||||
marginBottom: 14,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div className="font-orbitron" style={{ fontSize: 11, color: "#e5e7eb" }}>
|
||||
认证方案设计评审
|
||||
</div>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280", marginTop: 2 }}>
|
||||
design_review_20260304_141000 · 协作共识
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline (horizontal) */}
|
||||
<div style={{ marginBottom: 14, flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 0 }}>
|
||||
{steps.map((step, i) => (
|
||||
<div
|
||||
key={step.id}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Line before dot (except first) */}
|
||||
{i > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: "50%",
|
||||
top: 7,
|
||||
height: 3,
|
||||
background:
|
||||
steps[i - 1].status === "completed"
|
||||
? "linear-gradient(90deg,#00f0ff,#00ff9d)"
|
||||
: steps[i - 1].status === "active"
|
||||
? "linear-gradient(90deg,#00f0ff,#ff9500)"
|
||||
: "#111827",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{steps[i - 1].status === "active" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: "linear-gradient(90deg,transparent,rgba(0,240,255,0.6),transparent)",
|
||||
animation: "line-flow 2s linear infinite",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Line after dot (except last) */}
|
||||
{i < steps.length - 1 && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
right: 0,
|
||||
top: 7,
|
||||
height: 3,
|
||||
background:
|
||||
step.status === "completed"
|
||||
? "linear-gradient(90deg,#00ff9d,#00f0ff)"
|
||||
: step.status === "active"
|
||||
? "linear-gradient(90deg,#ff9500,rgba(0,240,255,0.2))"
|
||||
: "#111827",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Dot */}
|
||||
<div
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: "50%",
|
||||
border: `2px solid ${statusColors[step.status]}`,
|
||||
background:
|
||||
step.status === "completed"
|
||||
? "#00ff9d"
|
||||
: step.status === "active"
|
||||
? "#ff9500"
|
||||
: "#111827",
|
||||
zIndex: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
boxShadow: step.status !== "pending" ? `0 0 8px ${statusColors[step.status]}80` : "none",
|
||||
flexShrink: 0,
|
||||
transition: "all 0.3s",
|
||||
}}
|
||||
className={step.status === "active" ? "animate-scale-pulse" : ""}
|
||||
>
|
||||
{step.status === "completed" && (
|
||||
<svg width="8" height="8" viewBox="0 0 8 8">
|
||||
<path d="M1.5 4L3.5 6L6.5 2" stroke="#030712" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div
|
||||
className="font-mono-code"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
color: statusColors[step.status],
|
||||
marginTop: 6,
|
||||
textAlign: "center",
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{step.label}
|
||||
{step.time && (
|
||||
<div style={{ color: "#4b5563", marginTop: 1 }}>{step.time}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Attendees */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
|
||||
参会者
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
{attendees.map(a => (
|
||||
<div
|
||||
key={a.name}
|
||||
className="agent-avatar"
|
||||
style={{ background: a.gradient, width: 24, height: 24, fontSize: 9 }}
|
||||
>
|
||||
{a.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
|
||||
轮次 2/5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
235
frontend/src/components/RecentMeetingsCard.tsx
Normal file
235
frontend/src/components/RecentMeetingsCard.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { Calendar, CheckCircle, Clock } from "lucide-react";
|
||||
|
||||
interface Meeting {
|
||||
id: string;
|
||||
name: string;
|
||||
date: string;
|
||||
attendees: string[];
|
||||
iterations: number;
|
||||
consensus: string;
|
||||
status: "completed" | "ongoing" | "scheduled";
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const meetings: Meeting[] = [
|
||||
{
|
||||
id: "m1",
|
||||
name: "项目启动会议",
|
||||
date: "2026-03-04 09:00",
|
||||
attendees: ["CLA", "KIM", "OPC", "USR"],
|
||||
iterations: 2,
|
||||
consensus: "确定技术栈:React+Python+FastAPI",
|
||||
status: "completed",
|
||||
tags: ["架构", "启动"],
|
||||
},
|
||||
{
|
||||
id: "m2",
|
||||
name: "需求评审会议",
|
||||
date: "2026-03-04 11:30",
|
||||
attendees: ["CLA", "KIM", "USR"],
|
||||
iterations: 3,
|
||||
consensus: "MVP 功能范围确定,优先级排序完成",
|
||||
status: "completed",
|
||||
tags: ["需求", "PM"],
|
||||
},
|
||||
{
|
||||
id: "m3",
|
||||
name: "认证方案设计评审",
|
||||
date: "2026-03-04 14:10",
|
||||
attendees: ["CLA", "KIM", "OPC", "USR"],
|
||||
iterations: 2,
|
||||
consensus: "Session+Redis 方案,简单优先",
|
||||
status: "ongoing",
|
||||
tags: ["设计", "安全"],
|
||||
},
|
||||
{
|
||||
id: "m4",
|
||||
name: "代码审查会议",
|
||||
date: "2026-03-04 17:00",
|
||||
attendees: ["CLA", "OPC"],
|
||||
iterations: 0,
|
||||
consensus: "—",
|
||||
status: "scheduled",
|
||||
tags: ["代码", "审查"],
|
||||
},
|
||||
];
|
||||
|
||||
const statusConfig = {
|
||||
completed: { color: "#00ff9d", bg: "rgba(0,255,157,0.1)", label: "已完成" },
|
||||
ongoing: { color: "#ff9500", bg: "rgba(255,149,0,0.1)", label: "进行中" },
|
||||
scheduled: { color: "#6b7280", bg: "rgba(107,114,128,0.1)", label: "计划中" },
|
||||
};
|
||||
|
||||
const agentGradients: Record<string, string> = {
|
||||
CLA: "linear-gradient(135deg,#8b5cf6,#6366f1)",
|
||||
KIM: "linear-gradient(135deg,#f59e0b,#d97706)",
|
||||
OPC: "linear-gradient(135deg,#10b981,#059669)",
|
||||
USR: "linear-gradient(135deg,#f59e0b,#b45309)",
|
||||
};
|
||||
|
||||
export function RecentMeetingsCard() {
|
||||
return (
|
||||
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,240,255,0.1)",
|
||||
border: "1px solid rgba(0,240,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Calendar size={14} color="#00f0ff" />
|
||||
</div>
|
||||
<span className="card-title">会议记录</span>
|
||||
</div>
|
||||
<span className="font-mono-code" style={{ fontSize: 11, color: "#6b7280" }}>
|
||||
今日 4 场 · 2 已完成
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Meeting list */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, flex: 1 }}>
|
||||
{meetings.map(m => {
|
||||
const cfg = statusConfig[m.status];
|
||||
return (
|
||||
<div
|
||||
key={m.id}
|
||||
style={{
|
||||
background: "rgba(0,0,0,0.3)",
|
||||
border: `1px solid ${cfg.color}20`,
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
transition: "all 0.3s ease",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
(e.currentTarget as HTMLDivElement).style.borderColor = `${cfg.color}50`;
|
||||
(e.currentTarget as HTMLDivElement).style.boxShadow = `0 0 15px ${cfg.color}15`;
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
(e.currentTarget as HTMLDivElement).style.borderColor = `${cfg.color}20`;
|
||||
(e.currentTarget as HTMLDivElement).style.boxShadow = "none";
|
||||
}}
|
||||
>
|
||||
{/* Top row */}
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 8 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
className="font-orbitron"
|
||||
style={{ fontSize: 11, color: "#e5e7eb", fontWeight: 600, lineHeight: 1.3 }}
|
||||
>
|
||||
{m.name}
|
||||
</div>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 3 }}
|
||||
>
|
||||
<Clock size={9} color="#4b5563" />
|
||||
<span className="font-mono-code" style={{ fontSize: 9, color: "#4b5563" }}>
|
||||
{m.date}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
color: cfg.color,
|
||||
borderColor: `${cfg.color}40`,
|
||||
background: cfg.bg,
|
||||
flexShrink: 0,
|
||||
fontSize: 9,
|
||||
}}
|
||||
>
|
||||
{cfg.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Attendees */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<div style={{ display: "flex", gap: -4 }}>
|
||||
{m.attendees.map(a => (
|
||||
<div
|
||||
key={a}
|
||||
className="agent-avatar"
|
||||
style={{
|
||||
background: agentGradients[a],
|
||||
width: 20,
|
||||
height: 20,
|
||||
fontSize: 7,
|
||||
marginLeft: -4,
|
||||
border: "1px solid #0a0f1e",
|
||||
}}
|
||||
>
|
||||
{a}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{m.iterations > 0 && (
|
||||
<span className="font-mono-code" style={{ fontSize: 9, color: "#6b7280" }}>
|
||||
{m.iterations} 轮迭代
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Consensus */}
|
||||
{m.status !== "scheduled" && (
|
||||
<div
|
||||
style={{
|
||||
padding: "5px 8px",
|
||||
background: m.status === "completed" ? "rgba(0,255,157,0.05)" : "rgba(255,149,0,0.05)",
|
||||
border: `1px solid ${m.status === "completed" ? "rgba(0,255,157,0.15)" : "rgba(255,149,0,0.15)"}`,
|
||||
borderRadius: 8,
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<CheckCircle
|
||||
size={10}
|
||||
color={m.status === "completed" ? "#00ff9d" : "#ff9500"}
|
||||
style={{ flexShrink: 0, marginTop: 1 }}
|
||||
/>
|
||||
<span
|
||||
className="font-rajdhani"
|
||||
style={{ fontSize: 11, color: "#9ca3af", lineHeight: 1.3 }}
|
||||
>
|
||||
{m.consensus}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
<div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
|
||||
{m.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="font-mono-code"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
color: "#6b7280",
|
||||
background: "rgba(0,240,255,0.05)",
|
||||
border: "1px solid rgba(0,240,255,0.1)",
|
||||
borderRadius: 4,
|
||||
padding: "1px 6px",
|
||||
}}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
183
frontend/src/components/ResourceMonitorCard.tsx
Normal file
183
frontend/src/components/ResourceMonitorCard.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { Server, Lock, HardDrive } from "lucide-react";
|
||||
|
||||
interface Resource {
|
||||
label: string;
|
||||
value: number;
|
||||
max: number;
|
||||
unit: string;
|
||||
type: "cyan" | "green" | "amber";
|
||||
icon: React.ReactNode;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
const resources: Resource[] = [
|
||||
{
|
||||
label: "CPU 占用",
|
||||
value: 67,
|
||||
max: 100,
|
||||
unit: "%",
|
||||
type: "green",
|
||||
icon: <Server size={13} />,
|
||||
detail: "4核 / 8线程 · 负载 2.7",
|
||||
},
|
||||
{
|
||||
label: "内存使用",
|
||||
value: 58,
|
||||
max: 100,
|
||||
unit: "%",
|
||||
type: "amber",
|
||||
icon: <HardDrive size={13} />,
|
||||
detail: "9.3GB / 16GB",
|
||||
},
|
||||
{
|
||||
label: "文件锁",
|
||||
value: 5,
|
||||
max: 12,
|
||||
unit: "/12",
|
||||
type: "cyan",
|
||||
icon: <Lock size={13} />,
|
||||
detail: "src/auth/ · src/api/ · src/utils/",
|
||||
},
|
||||
];
|
||||
|
||||
const barColors = {
|
||||
cyan: "linear-gradient(90deg,#00f0ff,#8b5cf6)",
|
||||
green: "linear-gradient(90deg,#00ff9d,#00f0ff)",
|
||||
amber: "linear-gradient(90deg,#ff9500,#ff006e)",
|
||||
};
|
||||
|
||||
const textColors = {
|
||||
cyan: "#00f0ff",
|
||||
green: "#00ff9d",
|
||||
amber: "#ff9500",
|
||||
};
|
||||
|
||||
const lockedFiles = [
|
||||
{ path: "src/auth/login.py", agent: "CLA", time: "3m 21s" },
|
||||
{ path: "src/api/routes.py", agent: "OPC", time: "1m 05s" },
|
||||
{ path: "src/utils/crypto.py", agent: "CLA", time: "3m 21s" },
|
||||
];
|
||||
|
||||
export function ResourceMonitorCard() {
|
||||
return (
|
||||
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14, flexShrink: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,240,255,0.1)",
|
||||
border: "1px solid rgba(0,240,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Server size={14} color="#00f0ff" />
|
||||
</div>
|
||||
<span className="card-title">资源监控</span>
|
||||
</div>
|
||||
|
||||
{/* Resource bars */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12, marginBottom: 14, flexShrink: 0 }}>
|
||||
{resources.map(r => (
|
||||
<div key={r.label}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 5 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span style={{ color: textColors[r.type] }}>{r.icon}</span>
|
||||
<span className="font-mono-code" style={{ fontSize: 11, color: "#9ca3af" }}>
|
||||
{r.label}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="font-orbitron"
|
||||
style={{ fontSize: 12, fontWeight: 600, color: textColors[r.type] }}
|
||||
>
|
||||
{r.value}{r.unit}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 6,
|
||||
background: "#111827",
|
||||
borderRadius: 3,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${(r.value / r.max) * 100}%`,
|
||||
background: barColors[r.type],
|
||||
borderRadius: 3,
|
||||
transition: "width 0.5s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{r.detail && (
|
||||
<div className="font-mono-code" style={{ fontSize: 9, color: "#4b5563", marginTop: 3 }}>
|
||||
{r.detail}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* File locks */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<div
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 10, color: "#6b7280", marginBottom: 8 }}
|
||||
>
|
||||
活跃文件锁
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
{lockedFiles.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "6px 10px",
|
||||
background: "rgba(0,0,0,0.3)",
|
||||
border: "1px solid rgba(0,240,255,0.08)",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<Lock size={10} color="rgba(0,240,255,0.4)" style={{ flexShrink: 0 }} />
|
||||
<span
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 10, color: "#9ca3af", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||
>
|
||||
{f.path}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono-code"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
color: "#00f0ff",
|
||||
background: "rgba(0,240,255,0.1)",
|
||||
border: "1px solid rgba(0,240,255,0.2)",
|
||||
borderRadius: 4,
|
||||
padding: "1px 6px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{f.agent}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 9, color: "#4b5563", flexShrink: 0 }}
|
||||
>
|
||||
{f.time}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
151
frontend/src/components/StatisticsCard.tsx
Normal file
151
frontend/src/components/StatisticsCard.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { BarChart2, TrendingUp, CheckCircle, Clock } from "lucide-react";
|
||||
|
||||
const stats = [
|
||||
{ label: "已完成任务", value: "47", unit: "", color: "#00ff9d", icon: <CheckCircle size={14} /> },
|
||||
{ label: "进行中", value: "3", unit: "", color: "#00f0ff", icon: <Clock size={14} /> },
|
||||
{ label: "共识次数", value: "12", unit: "", color: "#8b5cf6", icon: <TrendingUp size={14} /> },
|
||||
{ label: "Token 消耗", value: "98.2", unit: "k", color: "#ff9500", icon: <BarChart2 size={14} /> },
|
||||
];
|
||||
|
||||
const weekData = [40, 65, 48, 80, 62, 75, 90];
|
||||
const weekLabels = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||||
|
||||
export function StatisticsCard() {
|
||||
const maxVal = Math.max(...weekData);
|
||||
|
||||
return (
|
||||
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16, flexShrink: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,240,255,0.1)",
|
||||
border: "1px solid rgba(0,240,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<BarChart2 size={14} color="#00f0ff" />
|
||||
</div>
|
||||
<span className="card-title">统计数据</span>
|
||||
</div>
|
||||
|
||||
{/* Main stat */}
|
||||
<div style={{ textAlign: "center", marginBottom: 16, flexShrink: 0 }}>
|
||||
<div
|
||||
className="font-orbitron"
|
||||
style={{
|
||||
fontSize: 40,
|
||||
fontWeight: 700,
|
||||
color: "#00f0ff",
|
||||
textShadow: "0 0 20px rgba(0,240,255,0.4)",
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
47
|
||||
</div>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280", marginTop: 4 }}>
|
||||
总完成任务
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginBottom: 16, flexShrink: 0 }}>
|
||||
{stats.map(stat => (
|
||||
<div
|
||||
key={stat.label}
|
||||
style={{
|
||||
background: "rgba(0,0,0,0.3)",
|
||||
border: `1px solid ${stat.color}20`,
|
||||
borderRadius: 10,
|
||||
padding: "10px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ color: stat.color, marginBottom: 4 }}>{stat.icon}</div>
|
||||
<div
|
||||
className="font-orbitron"
|
||||
style={{ fontSize: 18, fontWeight: 700, color: stat.color }}
|
||||
>
|
||||
{stat.value}
|
||||
<span style={{ fontSize: 12 }}>{stat.unit}</span>
|
||||
</div>
|
||||
<div className="font-mono-code" style={{ fontSize: 9, color: "#6b7280", marginTop: 2 }}>
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Weekly chart */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280", marginBottom: 8 }}>
|
||||
本周任务完成趋势
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
gap: 5,
|
||||
height: 60,
|
||||
}}
|
||||
>
|
||||
{weekData.map((v, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: `${(v / maxVal) * 52}px`,
|
||||
background: i === 6
|
||||
? "linear-gradient(180deg,#00f0ff,#8b5cf6)"
|
||||
: "rgba(0,240,255,0.2)",
|
||||
borderRadius: 3,
|
||||
border: i === 6 ? "1px solid rgba(0,240,255,0.3)" : "none",
|
||||
transition: "height 0.5s ease",
|
||||
}}
|
||||
/>
|
||||
<span className="font-mono-code" style={{ fontSize: 8, color: "#4b5563" }}>
|
||||
{weekLabels[i].slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success rate */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: "10px 12px",
|
||||
background: "rgba(0,255,157,0.05)",
|
||||
border: "1px solid rgba(0,255,157,0.15)",
|
||||
borderRadius: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
|
||||
成功率
|
||||
</span>
|
||||
<span className="font-orbitron" style={{ fontSize: 14, fontWeight: 700, color: "#00ff9d" }}>
|
||||
94.7%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
frontend/src/components/StatusBadge.tsx
Normal file
65
frontend/src/components/StatusBadge.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
// 状态徽章组件 - 统一的状态显示
|
||||
|
||||
import { statusColors, statusBgColors } from '../styles/dashboard';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: string;
|
||||
label?: string;
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
completed: '已完成',
|
||||
in_progress: '进行中',
|
||||
pending: '等待中',
|
||||
waiting: '等待中',
|
||||
working: '工作中',
|
||||
idle: '空闲',
|
||||
error: '错误',
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, label, size = 'md' }: StatusBadgeProps) {
|
||||
const color = statusColors[status] || '#666';
|
||||
const bgColor = statusBgColors[status] || '#66666620';
|
||||
const displayLabel = label || statusLabels[status] || status;
|
||||
|
||||
const padding = size === 'sm' ? '2px 8px' : '4px 10px';
|
||||
const fontSize = size === 'sm' ? 10 : 11;
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
fontSize,
|
||||
padding,
|
||||
borderRadius: 12,
|
||||
background: bgColor,
|
||||
color,
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
{displayLabel}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// 状态点组件
|
||||
interface StatusDotProps {
|
||||
status: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export function StatusDot({ status, size = 8 }: StatusDotProps) {
|
||||
const color = statusColors[status] || '#666';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '50%',
|
||||
background: color,
|
||||
boxShadow: `0 0 8px ${color}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
183
frontend/src/components/TaskInput.tsx
Normal file
183
frontend/src/components/TaskInput.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { Send, Zap, Code, FileText, Users, Cpu } from "lucide-react";
|
||||
|
||||
const quickTags = [
|
||||
{ icon: <Code size={11} />, label: "代码审查" },
|
||||
{ icon: <FileText size={11} />, label: "需求分析" },
|
||||
{ icon: <Zap size={11} />, label: "快速修复" },
|
||||
{ icon: <Users size={11} />, label: "团队会议" },
|
||||
{ icon: <Cpu size={11} />, label: "架构设计" },
|
||||
];
|
||||
|
||||
export function TaskInput() {
|
||||
const [value, setValue] = useState("");
|
||||
const [shaking, setShaking] = useState(false);
|
||||
const [priority, setPriority] = useState<"high" | "medium" | "low">("medium");
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!value.trim()) {
|
||||
setShaking(true);
|
||||
setTimeout(() => setShaking(false), 500);
|
||||
inputRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
setSubmitted(true);
|
||||
setTimeout(() => {
|
||||
setSubmitted(false);
|
||||
setValue("");
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleTag = (label: string) => {
|
||||
setValue(prev => (prev ? `${prev} [${label}]` : `[${label}] `));
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const priorityColors = {
|
||||
high: "#ff006e",
|
||||
medium: "#ff9500",
|
||||
low: "#6b7280",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={shaking ? "animate-shake" : ""}
|
||||
style={{
|
||||
background: "rgba(10,15,30,0.7)",
|
||||
border: "1px solid rgba(0,240,255,0.1)",
|
||||
borderRadius: 16,
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Top gradient line */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 2,
|
||||
background: "linear-gradient(90deg, transparent, #00f0ff, #8b5cf6, transparent)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ padding: "20px 24px" }}>
|
||||
{/* Input row */}
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: 12 }}>
|
||||
{/* Human avatar */}
|
||||
<div
|
||||
className="agent-avatar"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #f59e0b, #d97706)",
|
||||
marginTop: 6,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
H
|
||||
</div>
|
||||
|
||||
{/* Textarea */}
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) handleSubmit();
|
||||
}}
|
||||
placeholder="描述你的任务... Agent 将自动分析并协同完成(⌘+Enter 提交)"
|
||||
rows={2}
|
||||
style={{
|
||||
flex: 1,
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
resize: "none",
|
||||
color: "#e5e7eb",
|
||||
fontSize: 15,
|
||||
fontFamily: "'Noto Sans SC', sans-serif",
|
||||
lineHeight: 1.6,
|
||||
caretColor: "#00f0ff",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
marginTop: 14,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{/* Quick tags */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||
{quickTags.map(tag => (
|
||||
<button
|
||||
key={tag.label}
|
||||
className="quick-tag"
|
||||
onClick={() => handleTag(tag.label)}
|
||||
style={{ display: "flex", alignItems: "center", gap: 5 }}
|
||||
>
|
||||
{tag.icon}
|
||||
{tag.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right controls */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
{/* Priority selector */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
{(["high", "medium", "low"] as const).map(p => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setPriority(p)}
|
||||
className="font-mono-code"
|
||||
style={{
|
||||
fontSize: 10,
|
||||
padding: "4px 10px",
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${priority === p ? priorityColors[p] : "rgba(0,240,255,0.1)"}`,
|
||||
background: priority === p ? `${priorityColors[p]}20` : "transparent",
|
||||
color: priority === p ? priorityColors[p] : "#6b7280",
|
||||
cursor: "pointer",
|
||||
textTransform: "uppercase",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{p === "high" ? "高优先" : p === "medium" ? "中优先" : "低优先"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Submit button */}
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleSubmit}
|
||||
style={{
|
||||
padding: "10px 20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
background: submitted
|
||||
? "linear-gradient(135deg, #00ff9d, #00f0ff)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Send size={13} />
|
||||
{submitted ? "已提交!" : "提交任务"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
211
frontend/src/components/WorkflowCard.tsx
Normal file
211
frontend/src/components/WorkflowCard.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { GitBranch } from "lucide-react";
|
||||
|
||||
type NodeStatus = "completed" | "active" | "pending";
|
||||
|
||||
interface WorkflowNode {
|
||||
id: string;
|
||||
label: string;
|
||||
status: NodeStatus;
|
||||
duration?: string;
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
const nodes: WorkflowNode[] = [
|
||||
{ id: "n1", label: "需求收集", status: "completed", duration: "2m 14s", agent: "KIM" },
|
||||
{ id: "n2", label: "角色分配", status: "completed", duration: "0m 32s", agent: "SYS" },
|
||||
{ id: "n3", label: "需求评审会议", status: "completed", duration: "5m 03s", agent: "ALL" },
|
||||
{ id: "n4", label: "架构设计", status: "active", agent: "CLA" },
|
||||
{ id: "n5", label: "代码实现", status: "pending", agent: "OPC" },
|
||||
{ id: "n6", label: "代码审查会议", status: "pending", agent: "ALL" },
|
||||
{ id: "n7", label: "测试验证", status: "pending", agent: "OPC" },
|
||||
{ id: "n8", label: "部署上线", status: "pending", agent: "CLA" },
|
||||
];
|
||||
|
||||
const statusColors = {
|
||||
completed: "#00ff9d",
|
||||
active: "#ff9500",
|
||||
pending: "#374151",
|
||||
};
|
||||
|
||||
const statusBg = {
|
||||
completed: "#00ff9d",
|
||||
active: "#ff9500",
|
||||
pending: "#111827",
|
||||
};
|
||||
|
||||
export function WorkflowCard() {
|
||||
const completedCount = nodes.filter(n => n.status === "completed").length;
|
||||
|
||||
return (
|
||||
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14, flexShrink: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,240,255,0.1)",
|
||||
border: "1px solid rgba(0,240,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<GitBranch size={14} color="#00f0ff" />
|
||||
</div>
|
||||
<span className="card-title">工作流</span>
|
||||
</div>
|
||||
|
||||
{/* Progress summary */}
|
||||
<div style={{ marginBottom: 14, flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
|
||||
整体进度
|
||||
</span>
|
||||
<span className="font-mono-code" style={{ fontSize: 10, color: "#00f0ff" }}>
|
||||
{completedCount}/{nodes.length}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ height: 5, background: "#111827", borderRadius: 3, overflow: "hidden" }}>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${(completedCount / nodes.length) * 100}%`,
|
||||
background: "linear-gradient(90deg,#00f0ff,#8b5cf6)",
|
||||
borderRadius: 3,
|
||||
transition: "width 0.5s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Node list */}
|
||||
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 0 }}>
|
||||
{nodes.map((node, i) => (
|
||||
<div
|
||||
key={node.id}
|
||||
style={{ display: "flex", gap: 10, alignItems: "stretch" }}
|
||||
>
|
||||
{/* Left: dot + line */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
width: 20,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{/* Dot */}
|
||||
<div
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: "50%",
|
||||
border: `2px solid ${statusColors[node.status]}`,
|
||||
background: statusBg[node.status],
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
marginTop: 6,
|
||||
transition: "all 0.3s ease",
|
||||
boxShadow: node.status !== "pending" ? `0 0 8px ${statusColors[node.status]}60` : "none",
|
||||
}}
|
||||
className={node.status === "active" ? "animate-scale-pulse" : ""}
|
||||
>
|
||||
{node.status === "completed" && (
|
||||
<svg width="7" height="7" viewBox="0 0 7 7">
|
||||
<path d="M1 3.5L3 5.5L6 1.5" stroke="#030712" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Line */}
|
||||
{i < nodes.length - 1 && (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
width: 2,
|
||||
margin: "2px 0",
|
||||
borderRadius: 1,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
background: node.status === "completed"
|
||||
? "linear-gradient(180deg,#00f0ff,#00ff9d)"
|
||||
: node.status === "active"
|
||||
? "linear-gradient(180deg,#00f0ff,#ff9500)"
|
||||
: "#111827",
|
||||
}}
|
||||
>
|
||||
{node.status === "active" && (
|
||||
<div
|
||||
className="timeline-line-flow"
|
||||
style={{ position: "absolute", inset: 0, background: "transparent" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: content */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingBottom: i < nodes.length - 1 ? 10 : 0,
|
||||
paddingTop: 4,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span
|
||||
className="font-rajdhani"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color:
|
||||
node.status === "completed"
|
||||
? "#e5e7eb"
|
||||
: node.status === "active"
|
||||
? "#fff"
|
||||
: "#6b7280",
|
||||
fontWeight: node.status === "active" ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{node.label}
|
||||
</span>
|
||||
{node.agent && (
|
||||
<span
|
||||
className="font-mono-code"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
color: statusColors[node.status],
|
||||
background: `${statusColors[node.status]}15`,
|
||||
border: `1px solid ${statusColors[node.status]}30`,
|
||||
borderRadius: 4,
|
||||
padding: "1px 5px",
|
||||
}}
|
||||
>
|
||||
{node.agent}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{node.duration && (
|
||||
<div className="font-mono-code" style={{ fontSize: 9, color: "#4b5563", marginTop: 2 }}>
|
||||
{node.duration}
|
||||
</div>
|
||||
)}
|
||||
{node.status === "active" && (
|
||||
<div
|
||||
className="font-mono-code"
|
||||
style={{ fontSize: 9, color: "#ff9500", marginTop: 2 }}
|
||||
>
|
||||
<span className="animate-pulse-fast">● </span>进行中...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
frontend/src/components/dashboard/AgentStatusList.tsx
Normal file
50
frontend/src/components/dashboard/AgentStatusList.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
// Agent 状态列表组件
|
||||
|
||||
import type { Agent } from '../../types';
|
||||
import { StatusBadge, StatusDot } from '../StatusBadge';
|
||||
|
||||
interface AgentStatusListProps {
|
||||
agents: Agent[];
|
||||
}
|
||||
|
||||
export function AgentStatusList({ agents }: AgentStatusListProps) {
|
||||
if (agents.length === 0) {
|
||||
return (
|
||||
<p style={{ textAlign: 'center', color: 'rgba(255, 255, 255, 0.4)', padding: 24 }}>
|
||||
暂无 Agent 数据
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{agents.slice(0, 5).map((agent) => (
|
||||
<div
|
||||
key={agent.agent_id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 16px',
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<StatusDot status={agent.status} />
|
||||
<div>
|
||||
<p style={{ fontSize: 14, fontWeight: 500, color: '#fff', margin: 0 }}>
|
||||
{agent.name}
|
||||
</p>
|
||||
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.4)', margin: 0 }}>
|
||||
{agent.role} · {agent.model}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={agent.status} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
frontend/src/components/dashboard/RecentMeetingsList.tsx
Normal file
44
frontend/src/components/dashboard/RecentMeetingsList.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
// 最近会议列表组件
|
||||
|
||||
import type { Meeting } from '../../types';
|
||||
import { StatusBadge } from '../StatusBadge';
|
||||
|
||||
interface RecentMeetingsListProps {
|
||||
meetings: Meeting[];
|
||||
}
|
||||
|
||||
export function RecentMeetingsList({ meetings }: RecentMeetingsListProps) {
|
||||
if (meetings.length === 0) {
|
||||
return (
|
||||
<p style={{ textAlign: 'center', color: 'rgba(255, 255, 255, 0.4)', padding: 24 }}>
|
||||
暂无会议数据
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{meetings.slice(0, 5).map((meeting) => (
|
||||
<div
|
||||
key={meeting.meeting_id}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<p style={{ fontSize: 14, fontWeight: 500, color: '#fff', margin: 0 }}>
|
||||
{meeting.title}
|
||||
</p>
|
||||
<StatusBadge status={meeting.status} />
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
|
||||
进度: {meeting.progress_summary} · 参与者: {meeting.attendees.length} 人
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
frontend/src/components/dashboard/StatCard.tsx
Normal file
86
frontend/src/components/dashboard/StatCard.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
// 统计卡片组件
|
||||
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { colors, transitions } from '../../styles/dashboard';
|
||||
|
||||
interface StatCardProps {
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
value: string | number;
|
||||
subtitle?: string;
|
||||
color: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export function StatCard({ icon: Icon, title, value, subtitle, color, to }: StatCardProps) {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: 24,
|
||||
background: colors.background.card,
|
||||
borderRadius: 12,
|
||||
border: colors.border.default,
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
transition: transitions.default,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = color;
|
||||
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', alignItems: 'center', gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 10,
|
||||
background: `${color}20`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color,
|
||||
}}
|
||||
>
|
||||
<Icon size={24} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: colors.text.secondary,
|
||||
margin: 0,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 28,
|
||||
fontWeight: 700,
|
||||
color: colors.text.primary,
|
||||
margin: 0,
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
{subtitle && (
|
||||
<p style={{ fontSize: 12, color: colors.text.muted, margin: 0 }}>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ArrowRight size={18} color="rgba(255, 255, 255, 0.3)" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
13
frontend/src/components/index.ts
Normal file
13
frontend/src/components/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export { Header } from './Header';
|
||||
export { TaskInput } from './TaskInput';
|
||||
export { AgentStatusCard } from './AgentStatusCard';
|
||||
export { DiscussionCard } from './DiscussionCard';
|
||||
export { StatisticsCard } from './StatisticsCard';
|
||||
export { WorkflowCard } from './WorkflowCard';
|
||||
export { MeetingProgressCard } from './MeetingProgressCard';
|
||||
export { ResourceMonitorCard } from './ResourceMonitorCard';
|
||||
export { ConsensusCard } from './ConsensusCard';
|
||||
export { BarrierSyncCard } from './BarrierSyncCard';
|
||||
export { RecentMeetingsCard } from './RecentMeetingsCard';
|
||||
export { ActionBar } from './ActionBar';
|
||||
export { StatusBadge, StatusDot } from './StatusBadge';
|
||||
69
frontend/src/components/layout/AppLayout.tsx
Normal file
69
frontend/src/components/layout/AppLayout.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Sidebar } from './Sidebar';
|
||||
|
||||
export function AppLayout() {
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: '#030712' }}>
|
||||
{/* Sidebar */}
|
||||
<Sidebar />
|
||||
|
||||
{/* Main Content */}
|
||||
<main
|
||||
style={{
|
||||
flex: 1,
|
||||
marginLeft: 240,
|
||||
position: 'relative',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
{/* Background Grid */}
|
||||
<div className="bg-grid" />
|
||||
<div className="bg-scanline" />
|
||||
|
||||
{/* Ambient glow orbs */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '20%',
|
||||
left: '10%',
|
||||
width: 700,
|
||||
height: 700,
|
||||
borderRadius: '50%',
|
||||
background:
|
||||
'radial-gradient(circle,rgba(0,240,255,0.04) 0%,transparent 70%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '60%',
|
||||
right: '5%',
|
||||
width: 600,
|
||||
height: 600,
|
||||
borderRadius: '50%',
|
||||
background:
|
||||
'radial-gradient(circle,rgba(139,92,246,0.05) 0%,transparent 70%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Page Content */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
maxWidth: 1600,
|
||||
margin: '0 auto',
|
||||
padding: '24px',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
frontend/src/components/layout/Sidebar.tsx
Normal file
135
frontend/src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
Calendar,
|
||||
HardDrive,
|
||||
Workflow,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', icon: LayoutDashboard, label: '仪表盘' },
|
||||
{ path: '/agents', icon: Users, label: 'Agent 管理' },
|
||||
{ path: '/meetings', icon: Calendar, label: '会议管理' },
|
||||
{ path: '/resources', icon: HardDrive, label: '资源监控' },
|
||||
{ path: '/workflow', icon: Workflow, label: '工作流' },
|
||||
{ path: '/settings', icon: Settings, label: '配置' },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
return (
|
||||
<aside
|
||||
style={{
|
||||
width: 240,
|
||||
height: '100vh',
|
||||
background: 'rgba(3, 7, 18, 0.95)',
|
||||
borderRight: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div
|
||||
style={{
|
||||
padding: '24px 20px',
|
||||
borderBottom: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 0 20px rgba(0, 240, 255, 0.3)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
color: '#000',
|
||||
}}
|
||||
>
|
||||
S
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
color: '#fff',
|
||||
margin: 0,
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
Swarm Center
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: 'rgba(0, 240, 255, 0.7)',
|
||||
margin: 0,
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
多智能体协作系统
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav style={{ flex: 1, padding: '16px 12px' }}>
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
style={({ isActive }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '12px 16px',
|
||||
marginBottom: 4,
|
||||
borderRadius: 8,
|
||||
textDecoration: 'none',
|
||||
color: isActive ? '#00f0ff' : 'rgba(255, 255, 255, 0.7)',
|
||||
background: isActive
|
||||
? 'rgba(0, 240, 255, 0.1)'
|
||||
: 'transparent',
|
||||
borderLeft: isActive
|
||||
? '3px solid #00f0ff'
|
||||
: '3px solid transparent',
|
||||
transition: 'all 0.2s ease',
|
||||
})}
|
||||
>
|
||||
<item.icon size={20} />
|
||||
<span style={{ fontSize: 14, fontWeight: 500 }}>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Version */}
|
||||
<div
|
||||
style={{
|
||||
padding: '16px 20px',
|
||||
borderTop: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
}}
|
||||
>
|
||||
v0.1.0
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
2
frontend/src/components/layout/index.ts
Normal file
2
frontend/src/components/layout/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { AppLayout } from './AppLayout';
|
||||
export { Sidebar } from './Sidebar';
|
||||
428
frontend/src/lib/api.ts
Normal file
428
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
// API 客户端 - 与后端交互
|
||||
|
||||
import type {
|
||||
Agent,
|
||||
AgentState,
|
||||
FileLock,
|
||||
Heartbeat,
|
||||
Meeting,
|
||||
MeetingQueue,
|
||||
Workflow,
|
||||
AgentResourceStatus,
|
||||
} from '../types';
|
||||
|
||||
// API 基础地址
|
||||
const API_BASE = 'http://localhost:8000/api';
|
||||
|
||||
// 通用请求函数
|
||||
async function request<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
const url = `${API_BASE}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
error: {
|
||||
code: 'UNKNOWN_ERROR',
|
||||
message: `HTTP ${response.status}: ${response.statusText}`,
|
||||
},
|
||||
}));
|
||||
throw new Error(error.error?.message || '请求失败');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// ==================== Agent API ====================
|
||||
|
||||
export const agentApi = {
|
||||
// 列出所有 Agent
|
||||
list: () => request<{ agents: Agent[] }>('/agents'),
|
||||
|
||||
// 注册新 Agent
|
||||
register: (agent: Omit<Agent, 'status' | 'created_at'>) =>
|
||||
request<Agent>('/agents/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(agent),
|
||||
}),
|
||||
|
||||
// 获取 Agent 详情
|
||||
get: (agentId: string) => request<Agent>(`/agents/${agentId}`),
|
||||
|
||||
// 获取 Agent 状态
|
||||
getState: (agentId: string) =>
|
||||
request<AgentState>(`/agents/${agentId}/state`),
|
||||
|
||||
// 更新 Agent 状态
|
||||
updateState: (
|
||||
agentId: string,
|
||||
state: { task: string; progress: number; working_files?: string[] }
|
||||
) =>
|
||||
request<{ success: boolean }>(`/agents/${agentId}/state`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(state),
|
||||
}),
|
||||
};
|
||||
|
||||
// ==================== 文件锁 API ====================
|
||||
|
||||
export const lockApi = {
|
||||
// 获取所有锁
|
||||
list: () => request<{ locks: FileLock[] }>('/locks'),
|
||||
|
||||
// 获取文件锁
|
||||
acquire: (filePath: string, agentId: string, agentName: string) =>
|
||||
request<{ success: boolean }>('/locks/acquire', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ file_path: filePath, agent_id: agentId, agent_name: agentName }),
|
||||
}),
|
||||
|
||||
// 释放文件锁
|
||||
release: (filePath: string, agentId: string) =>
|
||||
request<{ success: boolean }>('/locks/release', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ file_path: filePath, agent_id: agentId }),
|
||||
}),
|
||||
|
||||
// 检查文件锁定状态
|
||||
check: (filePath: string) =>
|
||||
request<{ file_path: string; locked: boolean; locked_by?: string }>(
|
||||
`/locks/check?file_path=${encodeURIComponent(filePath)}`
|
||||
),
|
||||
};
|
||||
|
||||
// ==================== 心跳 API ====================
|
||||
|
||||
export const heartbeatApi = {
|
||||
// 获取所有心跳
|
||||
list: () => request<{ heartbeats: Record<string, Heartbeat> }>('/heartbeats'),
|
||||
|
||||
// 更新心跳
|
||||
update: (
|
||||
agentId: string,
|
||||
data: { status: Heartbeat['status']; current_task: string; progress: number }
|
||||
) =>
|
||||
request<{ success: boolean }>(`/heartbeats/${agentId}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
// 检查超时 Agent
|
||||
checkTimeouts: (timeoutSeconds: number = 60) =>
|
||||
request<{ timeout_seconds: number; timeout_agents: string[] }>(
|
||||
`/heartbeats/timeouts?timeout_seconds=${timeoutSeconds}`
|
||||
),
|
||||
};
|
||||
|
||||
// ==================== 会议 API ====================
|
||||
|
||||
export const meetingApi = {
|
||||
// 创建会议
|
||||
create: (data: {
|
||||
meeting_id: string;
|
||||
title: string;
|
||||
expected_attendees: string[];
|
||||
min_required?: number;
|
||||
}) =>
|
||||
request<{ success: boolean }>('/meetings/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
// 获取会议队列
|
||||
getQueue: (meetingId: string) =>
|
||||
request<MeetingQueue>(`/meetings/${meetingId}/queue`),
|
||||
|
||||
// 等待会议开始
|
||||
wait: (meetingId: string, agentId: string, timeout?: number) =>
|
||||
request<{ result: 'started' | 'timeout' | 'error'; meeting_id: string; agent_id: string }>(
|
||||
`/meetings/${meetingId}/wait`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ agent_id: agentId, timeout }),
|
||||
}
|
||||
),
|
||||
|
||||
// 结束会议
|
||||
end: (meetingId: string) =>
|
||||
request<{ success: boolean }>(`/meetings/${meetingId}/end`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// 创建会议记录
|
||||
createRecord: (data: {
|
||||
meeting_id: string;
|
||||
title: string;
|
||||
attendees: string[];
|
||||
steps?: string[];
|
||||
}) =>
|
||||
request<Meeting>('/meetings/record/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
// 添加讨论
|
||||
addDiscussion: (
|
||||
meetingId: string,
|
||||
data: { agent_id: string; agent_name: string; content: string; step?: string }
|
||||
) =>
|
||||
request<{ success: boolean }>(`/meetings/${meetingId}/discuss`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
// 更新进度
|
||||
updateProgress: (meetingId: string, step: string) =>
|
||||
request<{ success: boolean }>(`/meetings/${meetingId}/progress`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ step }),
|
||||
}),
|
||||
|
||||
// 获取会议详情
|
||||
get: (meetingId: string, date?: string) => {
|
||||
const query = date ? `?date=${date}` : '';
|
||||
return request<Meeting>(`/meetings/${meetingId}${query}`);
|
||||
},
|
||||
|
||||
// 完成会议
|
||||
finish: (meetingId: string, consensus: string) =>
|
||||
request<{ success: boolean }>(`/meetings/${meetingId}/finish`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ consensus }),
|
||||
}),
|
||||
|
||||
// 列出今日会议
|
||||
listToday: () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
return request<{ meetings: Meeting[] }>(`/meetings?date=${today}`);
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== 资源管理 API ====================
|
||||
|
||||
export const resourceApi = {
|
||||
// 执行任务
|
||||
execute: (agentId: string, task: string, timeout?: number) =>
|
||||
request<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
files_locked: string[];
|
||||
duration_seconds: number;
|
||||
}>('/execute', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ agent_id: agentId, task, timeout }),
|
||||
}),
|
||||
|
||||
// 获取所有 Agent 状态
|
||||
getAllStatus: () => request<{ agents: AgentResourceStatus[] }>('/status'),
|
||||
|
||||
// 解析任务文件
|
||||
parseTask: (task: string) =>
|
||||
request<{ task: string; files: string[] }>('/parse-task', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task }),
|
||||
}),
|
||||
};
|
||||
|
||||
// ==================== 工作流 API ====================
|
||||
|
||||
export const workflowApi = {
|
||||
// 列出工作流文件
|
||||
listFiles: () =>
|
||||
request<{ files: Array<{ name: string; path: string; size: number; modified: number }> }>('/workflows/files'),
|
||||
|
||||
// 列出已加载的工作流
|
||||
list: () =>
|
||||
request<{ workflows: Array<{ workflow_id: string; name: string; status: string; progress: string }> }>('/workflows/list'),
|
||||
|
||||
// 启动工作流
|
||||
start: (path: string) =>
|
||||
request<Workflow>(`/workflows/start/${path}`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// 获取工作流详情
|
||||
get: (workflowId: string) =>
|
||||
request<Workflow>(`/workflows/${workflowId}`),
|
||||
|
||||
// 获取工作流状态
|
||||
getStatus: (workflowId: string) =>
|
||||
request<Workflow>(`/workflows/${workflowId}/status`),
|
||||
|
||||
// 获取下一个节点
|
||||
getNext: (workflowId: string) =>
|
||||
request<{ meeting: { meeting_id: string; title: string; node_type: string; attendees: string[] } | null; message?: string }>(`/workflows/${workflowId}/next`),
|
||||
|
||||
// 标记节点完成
|
||||
complete: (workflowId: string, meetingId: string) =>
|
||||
request<{ success: boolean; message: string }>(`/workflows/${workflowId}/complete/${meetingId}`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// Agent 加入执行节点
|
||||
join: (workflowId: string, meetingId: string, agentId: string) =>
|
||||
request<{ status: 'ready' | 'waiting' | 'error'; progress: string; message: string; missing?: string[] }>(`/workflows/${workflowId}/join/${meetingId}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ agent_id: agentId }),
|
||||
}),
|
||||
|
||||
// 获取执行节点状态
|
||||
getExecutionStatus: (workflowId: string, meetingId: string) =>
|
||||
request<{ meeting_id: string; title: string; node_type: string; progress: string; is_ready: boolean; completed_attendees: string[]; missing: string[] }>(`/workflows/${workflowId}/execution/${meetingId}`),
|
||||
|
||||
// 强制跳转到指定节点
|
||||
jump: (workflowId: string, targetMeetingId: string) =>
|
||||
request<{ success: boolean; message: string; detail?: Workflow }>(`/workflows/${workflowId}/jump`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ target_meeting_id: targetMeetingId }),
|
||||
}),
|
||||
|
||||
// 处理节点失败
|
||||
handleFailure: (workflowId: string, meetingId: string) =>
|
||||
request<{ success: boolean; message: string; target?: string; detail?: Workflow }>(`/workflows/${workflowId}/fail/${meetingId}`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
};
|
||||
|
||||
// ==================== 角色分配 API ====================
|
||||
|
||||
export const roleApi = {
|
||||
// 获取任务主要角色
|
||||
getPrimary: (task: string) =>
|
||||
request<{
|
||||
task: string;
|
||||
primary_role: string;
|
||||
role_scores: Record<string, number>;
|
||||
}>('/roles/primary', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task }),
|
||||
}),
|
||||
|
||||
// 分配角色
|
||||
allocate: (task: string, agents: string[]) =>
|
||||
request<{
|
||||
task: string;
|
||||
primary_role: string;
|
||||
allocation: Record<string, string>;
|
||||
}>('/roles/allocate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task, agents }),
|
||||
}),
|
||||
|
||||
// 解释角色分配
|
||||
explain: (task: string, agents: string[]) =>
|
||||
request<{ explanation: string }>('/roles/explain', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task, agents }),
|
||||
}),
|
||||
};
|
||||
|
||||
// ==================== 系统 API ====================
|
||||
|
||||
export const systemApi = {
|
||||
// 健康检查
|
||||
health: () =>
|
||||
request<{
|
||||
status: string;
|
||||
version: string;
|
||||
services: Record<string, string>;
|
||||
}>('/health'),
|
||||
};
|
||||
|
||||
// ==================== 人类输入 API ====================
|
||||
|
||||
export const humanApi = {
|
||||
// 获取摘要
|
||||
summary: () =>
|
||||
request<{ participants: number; online_users: number; pending_tasks: number; urgent_tasks: number; pending_comments: number; last_updated: string }>('/humans/summary'),
|
||||
|
||||
// 注册参与者
|
||||
register: (userId: string, name: string, role?: string, avatar?: string) =>
|
||||
request<{ success: boolean; user_id: string }>('/humans/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ user_id: userId, name, role, avatar }),
|
||||
}),
|
||||
|
||||
// 获取参与者列表
|
||||
getParticipants: () =>
|
||||
request<{ participants: Array<{ id: string; name: string; role: string; status: string; avatar: string }> }>('/humans/participants'),
|
||||
|
||||
// 添加任务请求
|
||||
addTask: (content: string, options?: { from_user?: string; priority?: string; title?: string; target_files?: string[]; suggested_agent?: string; urgent?: boolean }) =>
|
||||
request<{ success: boolean; task_id: string }>('/humans/tasks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content, ...options }),
|
||||
}),
|
||||
|
||||
// 获取待处理任务
|
||||
getPendingTasks: (options?: { priority?: string; agent?: string }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.priority) params.append('priority', options.priority);
|
||||
if (options?.agent) params.append('agent', options.agent);
|
||||
return request<{ tasks: Array<{ id: string; from_user: string; timestamp: string; priority: string; title: string; content: string; suggested_agent: string; urgent: boolean; is_urgent: boolean }>; count: number }>(`/humans/tasks?${params}`);
|
||||
},
|
||||
|
||||
// 获取紧急任务
|
||||
getUrgentTasks: () =>
|
||||
request<{ tasks: Array<{ id: string; from_user: string; content: string; title: string; suggested_agent: string }>; count: number }>('/humans/tasks/urgent'),
|
||||
|
||||
// 标记任务处理中
|
||||
markTaskProcessing: (taskId: string) =>
|
||||
request<{ success: boolean }>(`/humans/tasks/${taskId}/processing`, {
|
||||
method: 'PUT',
|
||||
}),
|
||||
|
||||
// 标记任务完成
|
||||
markTaskComplete: (taskId: string) =>
|
||||
request<{ success: boolean }>(`/humans/tasks/${taskId}/complete`, {
|
||||
method: 'PUT',
|
||||
}),
|
||||
|
||||
// 添加会议评论
|
||||
addComment: (meetingId: string, content: string, options?: { from_user?: string; comment_type?: string; priority?: string }) =>
|
||||
request<{ success: boolean; comment_id: string }>('/humans/comments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ meeting_id: meetingId, content, ...options }),
|
||||
}),
|
||||
|
||||
// 获取待处理评论
|
||||
getPendingComments: (meetingId?: string) => {
|
||||
const params = meetingId ? `?meeting_id=${meetingId}` : '';
|
||||
return request<{ comments: Array<{ id: string; from_user: string; meeting_id: string; timestamp: string; type: string; priority: string; content: string }>; count: string }>(`/humans/comments${params}`);
|
||||
},
|
||||
|
||||
// 标记评论已处理
|
||||
markCommentAddressed: (commentId: string) =>
|
||||
request<{ success: boolean }>(`/humans/comments/${commentId}/addressed`, {
|
||||
method: 'PUT',
|
||||
}),
|
||||
|
||||
// 更新用户状态
|
||||
updateUserStatus: (userId: string, status: string, currentFocus?: string) =>
|
||||
request<{ success: boolean }>(`/humans/users/${userId}/status`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ status, current_focus: currentFocus }),
|
||||
}),
|
||||
};
|
||||
|
||||
// 导出所有 API
|
||||
export const api = {
|
||||
agent: agentApi,
|
||||
lock: lockApi,
|
||||
heartbeat: heartbeatApi,
|
||||
meeting: meetingApi,
|
||||
resource: resourceApi,
|
||||
workflow: workflowApi,
|
||||
role: roleApi,
|
||||
system: systemApi,
|
||||
human: humanApi,
|
||||
};
|
||||
|
||||
export default api;
|
||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './styles/index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
972
frontend/src/pages/AgentsPage.tsx
Normal file
972
frontend/src/pages/AgentsPage.tsx
Normal file
@@ -0,0 +1,972 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Users, Activity, Cpu, RefreshCw, Play, Square, Power } from 'lucide-react';
|
||||
import { api } from '../lib/api';
|
||||
import type { Agent, AgentState } from '../types';
|
||||
|
||||
// 注册 Agent 模态框
|
||||
function RegisterModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: {
|
||||
agent_id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
model: string;
|
||||
description: string;
|
||||
}) => void;
|
||||
}) {
|
||||
const [form, setForm] = useState({
|
||||
agent_id: '',
|
||||
name: '',
|
||||
role: 'developer',
|
||||
model: 'claude-opus-4.6',
|
||||
description: '',
|
||||
});
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 480,
|
||||
background: 'rgba(17, 24, 39, 0.95)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
padding: 24,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
color: '#fff',
|
||||
margin: 0,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
注册新 Agent
|
||||
</h2>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Agent ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.agent_id}
|
||||
onChange={(e) => setForm({ ...form, agent_id: e.target.value })}
|
||||
placeholder="例如: claude-001"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
placeholder="例如: Claude Code"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
角色
|
||||
</label>
|
||||
<select
|
||||
value={form.role}
|
||||
onChange={(e) => setForm({ ...form, role: e.target.value })}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
}}
|
||||
>
|
||||
<option value="architect">架构师 (architect)</option>
|
||||
<option value="pm">产品经理 (pm)</option>
|
||||
<option value="developer">开发者 (developer)</option>
|
||||
<option value="qa">测试工程师 (qa)</option>
|
||||
<option value="reviewer">代码审查者 (reviewer)</option>
|
||||
<option value="human">人类 (human)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
模型
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.model}
|
||||
onChange={(e) => setForm({ ...form, model: e.target.value })}
|
||||
placeholder="模型名称"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
描述
|
||||
</label>
|
||||
<textarea
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
placeholder="Agent 的职责描述"
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 20px',
|
||||
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',
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (form.agent_id && form.name) {
|
||||
onSubmit(form);
|
||||
onClose();
|
||||
setForm({
|
||||
agent_id: '',
|
||||
name: '',
|
||||
role: 'developer',
|
||||
model: 'claude-opus-4.6',
|
||||
description: '',
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 20px',
|
||||
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
color: '#000',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Agent 详情面板
|
||||
function AgentDetailPanel({
|
||||
agent,
|
||||
state,
|
||||
onClose,
|
||||
}: {
|
||||
agent: Agent;
|
||||
state: AgentState | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'working':
|
||||
return '#00ff9d';
|
||||
case 'idle':
|
||||
return '#00f0ff';
|
||||
case 'waiting':
|
||||
return '#ff9500';
|
||||
case 'error':
|
||||
return '#ff006e';
|
||||
default:
|
||||
return '#666';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 400,
|
||||
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: 24 }}>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#fff', margin: 0 }}>
|
||||
Agent 详情
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
fontSize: 24,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: 12,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 12,
|
||||
background: `linear-gradient(135deg, ${getStatusColor(agent.status)}40 0%, ${getStatusColor(agent.status)}10 100%)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Users size={32} color={getStatusColor(agent.status)} />
|
||||
</div>
|
||||
|
||||
<h3 style={{ fontSize: 18, fontWeight: 600, color: '#fff', margin: '0 0 4px 0' }}>
|
||||
{agent.name}
|
||||
</h3>
|
||||
<p style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}>
|
||||
{agent.agent_id}
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
borderRadius: 12,
|
||||
background: `${getStatusColor(agent.status)}20`,
|
||||
color: getStatusColor(agent.status),
|
||||
fontSize: 12,
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
{agent.status}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
borderRadius: 12,
|
||||
background: 'rgba(139, 92, 246, 0.2)',
|
||||
color: '#8b5cf6',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{agent.role}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<Cpu size={16} color="#00f0ff" />
|
||||
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}>模型</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 14, color: '#fff', margin: 0 }}>{agent.model}</p>
|
||||
</div>
|
||||
|
||||
{state && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<Activity size={16} color="#00ff9d" />
|
||||
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}>当前任务</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 14, color: '#fff', margin: 0 }}>{state.current_task || '无'}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}>进度</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 8,
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${state.progress}%`,
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: '#00f0ff', margin: '8px 0 0 0' }}>
|
||||
{state.progress}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{state.working_files.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}>工作文件</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{state.working_files.map((file, i) => (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: '#00f0ff',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
>
|
||||
{file}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}>最后更新</span>
|
||||
<p style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
|
||||
{new Date(state.last_update).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 运行中的 Agent 状态
|
||||
interface RunningAgent {
|
||||
agent_id: string;
|
||||
status: string;
|
||||
is_alive: boolean;
|
||||
uptime: number | null;
|
||||
restart_count: number;
|
||||
}
|
||||
|
||||
export function AgentsPage() {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [runningAgents, setRunningAgents] = useState<Record<string, RunningAgent>>({});
|
||||
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
||||
const [agentState, setAgentState] = useState<AgentState | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// API 基础 URL
|
||||
const API_BASE = 'http://localhost:8000/api';
|
||||
|
||||
// 加载 Agent 列表
|
||||
const loadAgents = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await api.agent.list();
|
||||
setAgents(res.agents);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载运行中的 Agent
|
||||
const loadRunningAgents = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/agents/control/list`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const runningMap: Record<string, RunningAgent> = {};
|
||||
data.forEach((agent: RunningAgent) => {
|
||||
runningMap[agent.agent_id] = agent;
|
||||
});
|
||||
setRunningAgents(runningMap);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载运行状态失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 启动 Agent
|
||||
const startAgent = async (agentId: string, agent: Agent) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/agents/control/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
agent_id: agentId,
|
||||
name: agent.name,
|
||||
role: agent.role,
|
||||
model: agent.model,
|
||||
agent_type: 'native_llm'
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
await loadRunningAgents();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(`启动失败: ${data.message || '未知错误'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`启动失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 停止 Agent
|
||||
const stopAgent = async (agentId: string) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/agents/control/stop`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
agent_id: agentId,
|
||||
graceful: true
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
await loadRunningAgents();
|
||||
} else {
|
||||
alert('停止失败');
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`停止失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载 Agent 状态
|
||||
const loadAgentState = async (agentId: string) => {
|
||||
try {
|
||||
const state = await api.agent.getState(agentId);
|
||||
setAgentState(state);
|
||||
} catch (err) {
|
||||
setAgentState(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadAgents();
|
||||
loadRunningAgents();
|
||||
const interval = setInterval(() => {
|
||||
loadAgents();
|
||||
loadRunningAgents();
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// 当选中 Agent 时加载状态
|
||||
useEffect(() => {
|
||||
if (selectedAgent) {
|
||||
loadAgentState(selectedAgent.agent_id);
|
||||
}
|
||||
}, [selectedAgent]);
|
||||
|
||||
// 注册 Agent
|
||||
const handleRegister = async (data: {
|
||||
agent_id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
model: string;
|
||||
description: string;
|
||||
}) => {
|
||||
try {
|
||||
await api.agent.register(data as Omit<Agent, 'status' | 'created_at'>);
|
||||
loadAgents();
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : '注册失败');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'working':
|
||||
return '#00ff9d';
|
||||
case 'idle':
|
||||
return '#00f0ff';
|
||||
case 'waiting':
|
||||
return '#ff9500';
|
||||
case 'error':
|
||||
return '#ff006e';
|
||||
default:
|
||||
return '#666';
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleLabel = (role: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
architect: '架构师',
|
||||
pm: '产品经理',
|
||||
developer: '开发者',
|
||||
qa: '测试工程师',
|
||||
reviewer: '审查者',
|
||||
human: '人类',
|
||||
};
|
||||
return labels[role] || role;
|
||||
};
|
||||
|
||||
if (loading && agents.length === 0) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
|
||||
<div style={{ color: '#00f0ff' }}>加载中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 添加脉动动画样式 */}
|
||||
<style>{`
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
`}</style>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 700, color: '#fff', margin: 0 }}>Agent 管理</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={loadAgents}
|
||||
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={() => setIsModalOpen(true)}
|
||||
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: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Plus size={18} />
|
||||
注册 Agent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
background: '#ff006e20',
|
||||
border: '1px solid #ff006e50',
|
||||
borderRadius: 8,
|
||||
color: '#ff006e',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
连接后端失败: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent Grid */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{agents.map((agent) => (
|
||||
<div
|
||||
key={agent.agent_id}
|
||||
onClick={() => setSelectedAgent(agent)}
|
||||
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', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 10,
|
||||
background: `linear-gradient(135deg, ${getStatusColor(agent.status)}40 0%, ${getStatusColor(agent.status)}10 100%)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Users size={24} color={getStatusColor(agent.status)} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: '#fff',
|
||||
margin: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{agent.name}
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
margin: '4px 0 0 0',
|
||||
}}
|
||||
>
|
||||
{agent.agent_id}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: getStatusColor(agent.status),
|
||||
boxShadow: `0 0 8px ${getStatusColor(agent.status)}`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: 12,
|
||||
background: `${getStatusColor(agent.status)}20`,
|
||||
color: getStatusColor(agent.status),
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
{agent.status}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: 12,
|
||||
background: 'rgba(139, 92, 246, 0.2)',
|
||||
color: '#8b5cf6',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
{getRoleLabel(agent.role)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: 0 }}>
|
||||
模型: {agent.model}
|
||||
</p>
|
||||
{/* 运行状态指示 */}
|
||||
{runningAgents[agent.agent_id] ? (
|
||||
<span style={{ fontSize: 11, color: '#00ff9d', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#00ff9d', animation: 'pulse 2s infinite' }} />
|
||||
运行中
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.3)' }}>
|
||||
已停止
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.3)', margin: '4px 0 0 0' }}>
|
||||
创建于: {new Date(agent.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
{/* 启动/停止按钮 */}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||
{runningAgents[agent.agent_id] ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
stopAgent(agent.agent_id);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(255, 0, 110, 0.2)',
|
||||
border: '1px solid rgba(255, 0, 110, 0.3)',
|
||||
borderRadius: 6,
|
||||
color: '#ff006e',
|
||||
fontSize: 12,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Square size={14} />
|
||||
停止
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startAgent(agent.agent_id, agent);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(0, 255, 157, 0.2)',
|
||||
border: '1px solid rgba(0, 255, 157, 0.3)',
|
||||
borderRadius: 6,
|
||||
color: '#00ff9d',
|
||||
fontSize: 12,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Play size={14} />
|
||||
启动
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* 显示运行时长 */}
|
||||
{runningAgents[agent.agent_id]?.uptime && (
|
||||
<p style={{ fontSize: 11, color: 'rgba(0, 255, 157, 0.6)', margin: '8px 0 0 0' }}>
|
||||
运行时长: {Math.floor(runningAgents[agent.agent_id].uptime! / 60)} 分钟
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{agents.length === 0 && !loading && (
|
||||
<div style={{ textAlign: 'center', padding: 80 }}>
|
||||
<Users size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
|
||||
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}>暂无 Agent</p>
|
||||
<p style={{ color: 'rgba(255, 255, 255, 0.3)', fontSize: 14 }}>点击"注册 Agent"创建</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
<RegisterModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} onSubmit={handleRegister} />
|
||||
|
||||
{selectedAgent && (
|
||||
<AgentDetailPanel
|
||||
agent={selectedAgent}
|
||||
state={agentState}
|
||||
onClose={() => {
|
||||
setSelectedAgent(null);
|
||||
setAgentState(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
238
frontend/src/pages/DashboardPage.tsx
Normal file
238
frontend/src/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Users, Calendar, HardDrive, Activity, Server } from 'lucide-react';
|
||||
import { api } from '../lib/api';
|
||||
import { StatCard } from '../components/dashboard/StatCard';
|
||||
import { AgentStatusList } from '../components/dashboard/AgentStatusList';
|
||||
import { RecentMeetingsList } from '../components/dashboard/RecentMeetingsList';
|
||||
import { colors, layout } from '../styles/dashboard';
|
||||
import type { Agent, Meeting, FileLock, Heartbeat } from '../types';
|
||||
|
||||
// 页面标题组件
|
||||
function PageHeader() {
|
||||
return (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 700, color: '#fff', margin: 0 }}>仪表盘</h1>
|
||||
<p style={{ fontSize: 14, color: colors.text.secondary, margin: '8px 0 0 0' }}>
|
||||
系统概览与实时监控
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 错误提示组件
|
||||
function ErrorAlert({ message }: { message: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
background: '#ff006e20',
|
||||
border: '1px solid #ff006e50',
|
||||
borderRadius: 8,
|
||||
color: '#ff006e',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
连接后端失败: {message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 统计卡片网格
|
||||
function StatsGrid({
|
||||
agents,
|
||||
meetings,
|
||||
locks,
|
||||
heartbeats,
|
||||
}: {
|
||||
agents: Agent[];
|
||||
meetings: Meeting[];
|
||||
locks: FileLock[];
|
||||
heartbeats: Record<string, Heartbeat>;
|
||||
}) {
|
||||
const workingAgents = agents.filter((a) => a.status === 'working').length;
|
||||
const activeMeetings = meetings.filter((m) => m.status === 'in_progress').length;
|
||||
const activeLocks = locks.length;
|
||||
const onlineAgents = Object.values(heartbeats).filter((h) => !h.is_timeout).length;
|
||||
|
||||
return (
|
||||
<div style={layout.grid.stats}>
|
||||
<StatCard
|
||||
icon={Users}
|
||||
title="Agent 总数"
|
||||
value={agents.length}
|
||||
subtitle={`${workingAgents} 个工作中`}
|
||||
color="#00f0ff"
|
||||
to="/agents"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Calendar}
|
||||
title="今日会议"
|
||||
value={meetings.length}
|
||||
subtitle={`${activeMeetings} 个进行中`}
|
||||
color="#8b5cf6"
|
||||
to="/meetings"
|
||||
/>
|
||||
<StatCard
|
||||
icon={HardDrive}
|
||||
title="文件锁"
|
||||
value={activeLocks}
|
||||
subtitle="当前锁定中"
|
||||
color="#ff9500"
|
||||
to="/resources"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Activity}
|
||||
title="在线 Agent"
|
||||
value={onlineAgents}
|
||||
subtitle="心跳正常"
|
||||
color="#00ff9d"
|
||||
to="/resources"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 面板卡片组件
|
||||
function PanelCard({
|
||||
title,
|
||||
children,
|
||||
action,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: colors.background.card,
|
||||
borderRadius: 12,
|
||||
border: colors.border.default,
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: 0 }}>{title}</h3>
|
||||
{action}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 系统状态面板
|
||||
function SystemStatusPanel() {
|
||||
const items = [
|
||||
{ label: '后端服务', value: '运行中', color: '#00ff9d' },
|
||||
{ label: 'API 地址', value: 'localhost:8000', color: '#00f0ff' },
|
||||
{ label: '前端版本', value: 'v0.1.0', color: '#fff' },
|
||||
{ label: '自动刷新', value: '10 秒', color: '#8b5cf6' },
|
||||
];
|
||||
|
||||
return (
|
||||
<PanelCard title="系统状态" action={<Server size={18} color="rgba(255, 255, 255, 0.4)" />}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{items.map((item) => (
|
||||
<div key={item.label} style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: 13, color: colors.text.subtle }}>{item.label}</span>
|
||||
<span style={{ fontSize: 13, color: item.color }}>{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PanelCard>
|
||||
);
|
||||
}
|
||||
|
||||
// 查看全部链接
|
||||
function ViewAllLink({ to }: { to: string }) {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#00f0ff',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
查看全部 →
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
function LoadingState() {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
|
||||
<div style={{ color: '#00f0ff' }}>加载中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [meetings, setMeetings] = useState<Meeting[]>([]);
|
||||
const [locks, setLocks] = useState<FileLock[]>([]);
|
||||
const [heartbeats, setHeartbeats] = useState<Record<string, Heartbeat>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [agentsRes, meetingsRes, locksRes, heartbeatsRes] = await Promise.all([
|
||||
api.agent.list().catch(() => ({ agents: [] })),
|
||||
api.meeting.listToday().catch(() => ({ meetings: [] })),
|
||||
api.lock.list().catch(() => ({ locks: [] })),
|
||||
api.heartbeat.list().catch(() => ({ heartbeats: {} })),
|
||||
]);
|
||||
setAgents(agentsRes.agents);
|
||||
setMeetings(meetingsRes.meetings);
|
||||
setLocks(locksRes.locks);
|
||||
setHeartbeats(heartbeatsRes.heartbeats);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (loading) return <LoadingState />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader />
|
||||
{error && <ErrorAlert message={error} />}
|
||||
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<StatsGrid agents={agents} meetings={meetings} locks={locks} heartbeats={heartbeats} />
|
||||
</div>
|
||||
|
||||
<div style={layout.grid.main}>
|
||||
<PanelCard title="Agent 状态" action={<ViewAllLink to="/agents" />}>
|
||||
<AgentStatusList agents={agents} />
|
||||
</PanelCard>
|
||||
|
||||
<PanelCard title="最近会议" action={<ViewAllLink to="/meetings" />}>
|
||||
<RecentMeetingsList meetings={meetings} />
|
||||
</PanelCard>
|
||||
|
||||
<SystemStatusPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
739
frontend/src/pages/MeetingsPage.tsx
Normal file
739
frontend/src/pages/MeetingsPage.tsx
Normal file
@@ -0,0 +1,739 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Calendar, Users, MessageSquare, CheckCircle, Play } from 'lucide-react';
|
||||
import { api } from '../lib/api';
|
||||
import type { Meeting, Discussion } from '../types';
|
||||
|
||||
// 创建会议模态框
|
||||
function CreateMeetingModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: {
|
||||
meeting_id: string;
|
||||
title: string;
|
||||
expected_attendees: string[];
|
||||
steps: string[];
|
||||
}) => void;
|
||||
}) {
|
||||
const [form, setForm] = useState({
|
||||
meeting_id: '',
|
||||
title: '',
|
||||
expected_attendees: '',
|
||||
steps: '收集想法\n讨论迭代\n生成共识',
|
||||
});
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 520,
|
||||
background: 'rgba(17, 24, 39, 0.95)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
padding: 24,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#fff', margin: '0 0 20px 0' }}>
|
||||
创建会议
|
||||
</h2>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: 'rgba(255, 255, 255, 0.6)', marginBottom: 6 }}>
|
||||
会议 ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.meeting_id}
|
||||
onChange={(e) => setForm({ ...form, meeting_id: e.target.value })}
|
||||
placeholder="例如: design-review-001"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: 'rgba(255, 255, 255, 0.6)', marginBottom: 6 }}>
|
||||
会议标题
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.title}
|
||||
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
||||
placeholder="例如: 设计评审会议"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: 'rgba(255, 255, 255, 0.6)', marginBottom: 6 }}>
|
||||
参与者 (逗号分隔)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.expected_attendees}
|
||||
onChange={(e) => setForm({ ...form, expected_attendees: e.target.value })}
|
||||
placeholder="claude-001, kimi-002, opencode-003"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: 'rgba(255, 255, 255, 0.6)', marginBottom: 6 }}>
|
||||
会议步骤 (每行一个)
|
||||
</label>
|
||||
<textarea
|
||||
value={form.steps}
|
||||
onChange={(e) => setForm({ ...form, steps: e.target.value })}
|
||||
rows={4}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 20px',
|
||||
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',
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (form.meeting_id && form.title) {
|
||||
onSubmit({
|
||||
meeting_id: form.meeting_id,
|
||||
title: form.title,
|
||||
expected_attendees: form.expected_attendees.split(',').map(s => s.trim()).filter(Boolean),
|
||||
steps: form.steps.split('\n').map(s => s.trim()).filter(Boolean),
|
||||
});
|
||||
onClose();
|
||||
setForm({
|
||||
meeting_id: '',
|
||||
title: '',
|
||||
expected_attendees: '',
|
||||
steps: '收集想法\n讨论迭代\n生成共识',
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 20px',
|
||||
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
color: '#000',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 会议详情面板
|
||||
function MeetingDetailPanel({
|
||||
meeting,
|
||||
onClose,
|
||||
}: {
|
||||
meeting: Meeting;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [discussions, setDiscussions] = useState<Discussion[]>(meeting.discussions || []);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [agentName, setAgentName] = useState('');
|
||||
|
||||
const handleAddDiscussion = async () => {
|
||||
if (!newMessage.trim() || !agentName.trim()) return;
|
||||
|
||||
try {
|
||||
await api.meeting.addDiscussion(meeting.meeting_id, {
|
||||
agent_id: 'user-' + Date.now(),
|
||||
agent_name: agentName,
|
||||
content: newMessage,
|
||||
step: meeting.steps?.find(s => s.status === 'active')?.label || meeting.steps?.[0]?.label || '',
|
||||
});
|
||||
setDiscussions([...discussions, {
|
||||
agent_id: 'user-' + Date.now(),
|
||||
agent_name: agentName,
|
||||
content: newMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
step: meeting.steps.find(s => s.status === 'active')?.label || '',
|
||||
}]);
|
||||
setNewMessage('');
|
||||
} catch (err) {
|
||||
console.error('Failed to add discussion:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = async () => {
|
||||
const consensus = prompt('请输入会议共识:');
|
||||
if (consensus) {
|
||||
try {
|
||||
await api.meeting.finish(meeting.meeting_id, consensus);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
alert('结束会议失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Meeting 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' }}>
|
||||
{meeting.title}
|
||||
</h3>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: 0 }}>
|
||||
{meeting.meeting_id} · {meeting.date}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
borderRadius: 12,
|
||||
background:
|
||||
meeting.status === 'completed'
|
||||
? '#00ff9d20'
|
||||
: meeting.status === 'in_progress'
|
||||
? '#00f0ff20'
|
||||
: '#ff950020',
|
||||
color:
|
||||
meeting.status === 'completed'
|
||||
? '#00ff9d'
|
||||
: meeting.status === 'in_progress'
|
||||
? '#00f0ff'
|
||||
: '#ff9500',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{meeting.status === 'completed' ? '已完成' : meeting.status === 'in_progress' ? '进行中' : '等待中'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, color: 'rgba(255, 255, 255, 0.7)', margin: '0 0 12px 0' }}>
|
||||
会议步骤
|
||||
</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{(meeting.steps || []).map((step, index) => (
|
||||
<div
|
||||
key={step.step_id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '12px 16px',
|
||||
background:
|
||||
step.status === 'completed'
|
||||
? 'rgba(0, 255, 157, 0.1)'
|
||||
: step.status === 'active'
|
||||
? 'rgba(0, 240, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
border:
|
||||
step.status === 'active'
|
||||
? '1px solid rgba(0, 240, 255, 0.3)'
|
||||
: '1px solid transparent',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background:
|
||||
step.status === 'completed'
|
||||
? '#00ff9d'
|
||||
: step.status === 'active'
|
||||
? '#00f0ff'
|
||||
: 'rgba(255, 255, 255, 0.1)',
|
||||
color: step.status === 'pending' ? 'rgba(255, 255, 255, 0.4)' : '#000',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{step.status === 'completed' ? <CheckCircle size={14} /> : index + 1}
|
||||
</div>
|
||||
<span style={{ fontSize: 14, color: '#fff', flex: 1 }}>{step.label}</span>
|
||||
{step.status === 'active' && (
|
||||
<Play size={14} color="#00f0ff" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Discussions */}
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
<h4 style={{ fontSize: 14, fontWeight: 600, color: 'rgba(255, 255, 255, 0.7)', margin: '0 0 12px 0' }}>
|
||||
讨论记录 ({discussions.length})
|
||||
</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginBottom: 16 }}>
|
||||
{discussions.map((disc, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
padding: 12,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
borderLeft: '3px solid #00f0ff',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#00f0ff' }}>{disc.agent_name}</span>
|
||||
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.3)' }}>
|
||||
{new Date(disc.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.8)', margin: 0 }}>{disc.content}</p>
|
||||
{disc.step && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 10,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
background: 'rgba(139, 92, 246, 0.2)',
|
||||
color: '#8b5cf6',
|
||||
marginTop: 4,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
{disc.step}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{discussions.length === 0 && (
|
||||
<p style={{ textAlign: 'center', color: 'rgba(255, 255, 255, 0.3)', padding: 24 }}>
|
||||
暂无讨论记录
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Discussion */}
|
||||
<div style={{ marginTop: 'auto', paddingTop: 16, borderTop: '1px solid rgba(255, 255, 255, 0.1)' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="你的名称"
|
||||
value={agentName}
|
||||
onChange={(e) => setAgentName(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 6,
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
marginBottom: 8,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="添加讨论..."
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddDiscussion()}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '10px 14px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddDiscussion}
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
background: '#00f0ff',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
color: '#000',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{meeting.status !== 'completed' && (
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 16 }}>
|
||||
<button
|
||||
onClick={handleFinish}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 20px',
|
||||
background: '#00ff9d',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
color: '#000',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
结束会议
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeetingsPage() {
|
||||
const [meetings, setMeetings] = useState<Meeting[]>([]);
|
||||
const [selectedMeeting, setSelectedMeeting] = useState<Meeting | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadMeetings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await api.meeting.listToday();
|
||||
setMeetings(res.meetings);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadMeetings();
|
||||
const interval = setInterval(loadMeetings, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const handleCreate = async (data: {
|
||||
meeting_id: string;
|
||||
title: string;
|
||||
expected_attendees: string[];
|
||||
steps: string[];
|
||||
}) => {
|
||||
try {
|
||||
// 创建会议记录
|
||||
await api.meeting.createRecord({
|
||||
meeting_id: data.meeting_id,
|
||||
title: data.title,
|
||||
attendees: data.expected_attendees,
|
||||
steps: data.steps,
|
||||
});
|
||||
loadMeetings();
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : '创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return '#00ff9d';
|
||||
case 'in_progress':
|
||||
return '#00f0ff';
|
||||
case 'waiting':
|
||||
return '#ff9500';
|
||||
default:
|
||||
return '#666';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && meetings.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>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
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: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Plus size={18} />
|
||||
创建会议
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
background: '#ff006e20',
|
||||
border: '1px solid #ff006e50',
|
||||
borderRadius: 8,
|
||||
color: '#ff006e',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
连接后端失败: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Meetings Grid */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{meetings.map((meeting) => (
|
||||
<div
|
||||
key={meeting.meeting_id}
|
||||
onClick={() => setSelectedMeeting(meeting)}
|
||||
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 }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: 0 }}>
|
||||
{meeting.title}
|
||||
</h3>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: 12,
|
||||
background: `${getStatusColor(meeting.status)}20`,
|
||||
color: getStatusColor(meeting.status),
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
{meeting.status === 'completed' ? '已完成' : meeting.status === 'in_progress' ? '进行中' : '等待中'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '0 0 16px 0' }}>
|
||||
{meeting.meeting_id}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Users size={14} color="rgba(255, 255, 255, 0.4)" />
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.6)' }}>
|
||||
{meeting.attendees?.length || 0} 位参与者
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<CheckCircle size={14} color="rgba(255, 255, 255, 0.4)" />
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.6)' }}>
|
||||
进度: {meeting.progress_summary}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<MessageSquare size={14} color="rgba(255, 255, 255, 0.4)" />
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.6)' }}>
|
||||
{meeting.discussions?.length || 0} 条讨论
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
height: 4,
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: meeting.progress_summary,
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{meetings.length === 0 && !loading && (
|
||||
<div style={{ textAlign: 'center', padding: 80 }}>
|
||||
<Calendar 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 }}>点击"创建会议"开始</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
<CreateMeetingModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} onSubmit={handleCreate} />
|
||||
|
||||
{selectedMeeting && (
|
||||
<MeetingDetailPanel meeting={selectedMeeting} onClose={() => setSelectedMeeting(null)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
362
frontend/src/pages/ResourcesPage.tsx
Normal file
362
frontend/src/pages/ResourcesPage.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { HardDrive, Lock, Unlock, Activity, Clock, FileText, RefreshCw } from 'lucide-react';
|
||||
import { api } from '../lib/api';
|
||||
import type { FileLock, Heartbeat, AgentResourceStatus } from '../types';
|
||||
|
||||
export function ResourcesPage() {
|
||||
const [locks, setLocks] = useState<FileLock[]>([]);
|
||||
const [heartbeats, setHeartbeats] = useState<Record<string, Heartbeat>>({});
|
||||
const [statuses, setStatuses] = useState<AgentResourceStatus[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'locks' | 'heartbeats' | 'status'>('locks');
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [locksRes, heartbeatsRes, statusRes] = await Promise.all([
|
||||
api.lock.list().catch(() => ({ locks: [] })),
|
||||
api.heartbeat.list().catch(() => ({ heartbeats: {} })),
|
||||
api.resource.getAllStatus().catch(() => ({ agents: [] })),
|
||||
]);
|
||||
setLocks(locksRes.locks);
|
||||
setHeartbeats(heartbeatsRes.heartbeats);
|
||||
setStatuses(statusRes.agents);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
const interval = setInterval(loadData, 5000); // 5秒刷新一次
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const handleReleaseLock = async (filePath: string, agentId: string) => {
|
||||
if (!confirm(`确定要释放 ${filePath} 的锁吗?`)) return;
|
||||
try {
|
||||
await api.lock.release(filePath, agentId);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
alert('释放失败: ' + (err instanceof Error ? err.message : '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'working':
|
||||
return '#00ff9d';
|
||||
case 'idle':
|
||||
return '#00f0ff';
|
||||
case 'waiting':
|
||||
return '#ff9500';
|
||||
case 'error':
|
||||
return '#ff006e';
|
||||
default:
|
||||
return '#666';
|
||||
}
|
||||
};
|
||||
|
||||
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' }}>
|
||||
监控文件锁、Agent 心跳和系统状态
|
||||
</p>
|
||||
</div>
|
||||
{loading && (
|
||||
<span style={{ fontSize: 12, color: '#00f0ff', marginRight: 12 }}>加载中...</span>
|
||||
)}
|
||||
<button
|
||||
onClick={loadData}
|
||||
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>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
background: '#ff006e20',
|
||||
border: '1px solid #ff006e50',
|
||||
borderRadius: 8,
|
||||
color: '#ff006e',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
连接后端失败: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 24 }}>
|
||||
{[
|
||||
{ id: 'locks', label: '文件锁', icon: Lock, count: locks.length },
|
||||
{ id: 'heartbeats', label: '心跳状态', icon: Activity, count: Object.keys(heartbeats).length },
|
||||
{ id: 'status', label: 'Agent 状态', icon: HardDrive, count: statuses.length },
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as typeof activeTab)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '12px 20px',
|
||||
background: activeTab === tab.id ? 'rgba(0, 240, 255, 0.1)' : 'rgba(255, 255, 255, 0.05)',
|
||||
border: `1px solid ${activeTab === tab.id ? 'rgba(0, 240, 255, 0.3)' : 'rgba(255, 255, 255, 0.1)'}`,
|
||||
borderRadius: 8,
|
||||
color: activeTab === tab.id ? '#00f0ff' : 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<tab.icon size={16} />
|
||||
{tab.label}
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
padding: '2px 8px',
|
||||
background: activeTab === tab.id ? 'rgba(0, 240, 255, 0.2)' : 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 10,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{tab.count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{activeTab === 'locks' && (
|
||||
<div style={{ background: 'rgba(17, 24, 39, 0.7)', borderRadius: 12, border: '1px solid rgba(0, 240, 255, 0.1)' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(0, 240, 255, 0.1)' }}>
|
||||
<th style={{ padding: '16px 20px', textAlign: 'left', fontSize: 12, color: 'rgba(255, 255, 255, 0.5)', fontWeight: 500 }}>文件路径</th>
|
||||
<th style={{ padding: '16px 20px', textAlign: 'left', fontSize: 12, color: 'rgba(255, 255, 255, 0.5)', fontWeight: 500 }}>锁定者</th>
|
||||
<th style={{ padding: '16px 20px', textAlign: 'left', fontSize: 12, color: 'rgba(255, 255, 255, 0.5)', fontWeight: 500 }}>锁定时间</th>
|
||||
<th style={{ padding: '16px 20px', textAlign: 'center', fontSize: 12, color: 'rgba(255, 255, 255, 0.5)', fontWeight: 500 }}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{locks.map((lock, index) => (
|
||||
<tr key={index} style={{ borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||
<td style={{ padding: '14px 20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<FileText size={16} color="#00f0ff" />
|
||||
<span style={{ fontSize: 14, color: '#fff', fontFamily: 'monospace' }}>{lock.file_path}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '14px 20px' }}>
|
||||
<span style={{ fontSize: 14, color: '#fff' }}>{lock.agent_name}</span>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', marginLeft: 8 }}>({lock.agent_id})</span>
|
||||
</td>
|
||||
<td style={{ padding: '14px 20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Clock size={14} color="rgba(255, 255, 255, 0.4)" />
|
||||
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.7)' }}>{lock.elapsed_display}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '14px 20px', textAlign: 'center' }}>
|
||||
<button
|
||||
onClick={() => handleReleaseLock(lock.file_path, lock.agent_id)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#ff006e20',
|
||||
border: '1px solid #ff006e50',
|
||||
borderRadius: 6,
|
||||
color: '#ff006e',
|
||||
fontSize: 12,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
释放
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{locks.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: 60 }}>
|
||||
<Unlock size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
|
||||
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}>暂无文件锁</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'heartbeats' && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 16 }}>
|
||||
{Object.entries(heartbeats).map(([agentId, hb]) => (
|
||||
<div
|
||||
key={agentId}
|
||||
style={{
|
||||
padding: 20,
|
||||
background: 'rgba(17, 24, 39, 0.7)',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${hb.is_timeout ? '#ff006e50' : 'rgba(0, 240, 255, 0.1)'}`,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: '#fff' }}>{agentId}</span>
|
||||
{hb.is_timeout ? (
|
||||
<span style={{ padding: '4px 10px', borderRadius: 12, background: '#ff006e20', color: '#ff006e', fontSize: 11 }}>
|
||||
超时
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ padding: '4px 10px', borderRadius: 12, background: '#00ff9d20', color: '#00ff9d', fontSize: 11 }}>
|
||||
正常
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}>状态</span>
|
||||
<span style={{ fontSize: 12, color: getStatusColor(hb.status) }}>{hb.status}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}>当前任务</span>
|
||||
<span style={{ fontSize: 12, color: '#fff' }}>{hb.current_task || '-'}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}>进度</span>
|
||||
<span style={{ fontSize: 12, color: '#00f0ff' }}>{hb.progress}%</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}>运行时间</span>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.7)' }}>{hb.elapsed_display}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||
<div
|
||||
style={{
|
||||
height: 4,
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${hb.progress}%`,
|
||||
height: '100%',
|
||||
background: hb.is_timeout ? '#ff006e' : '#00ff9d',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{Object.keys(heartbeats).length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: 60, gridColumn: '1 / -1' }}>
|
||||
<Activity size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
|
||||
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}>暂无心跳数据</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'status' && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))', gap: 16 }}>
|
||||
{statuses.map((status) => (
|
||||
<div
|
||||
key={status.agent_id}
|
||||
style={{
|
||||
padding: 20,
|
||||
background: 'rgba(17, 24, 39, 0.7)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<div>
|
||||
<h4 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: 0 }}>{status.info.name}</h4>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>{status.agent_id}</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: 12,
|
||||
background: `${getStatusColor(status.heartbeat.status)}20`,
|
||||
color: getStatusColor(status.heartbeat.status),
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
{status.heartbeat.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div style={{ padding: 12, background: 'rgba(0, 0, 0, 0.2)', borderRadius: 8 }}>
|
||||
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)', margin: '0 0 4px 0' }}>角色</p>
|
||||
<p style={{ fontSize: 13, color: '#fff', margin: 0 }}>{status.info.role}</p>
|
||||
</div>
|
||||
<div style={{ padding: 12, background: 'rgba(0, 0, 0, 0.2)', borderRadius: 8 }}>
|
||||
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)', margin: '0 0 4px 0' }}>模型</p>
|
||||
<p style={{ fontSize: 13, color: '#fff', margin: 0 }}>{status.info.model}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, padding: 12, background: 'rgba(0, 0, 0, 0.2)', borderRadius: 8 }}>
|
||||
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)', margin: '0 0 4px 0' }}>当前任务</p>
|
||||
<p style={{ fontSize: 13, color: '#00f0ff', margin: 0 }}>{status.state.task || '-'}</p>
|
||||
</div>
|
||||
|
||||
{status.locks.length > 0 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)', margin: '0 0 8px 0' }}>
|
||||
文件锁 ({status.locks.length})
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{status.locks.map((lock, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Lock size={12} color="#ff9500" />
|
||||
<span style={{ fontSize: 12, color: '#ff9500', fontFamily: 'monospace' }}>{lock.file}</span>
|
||||
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.4)' }}>({lock.elapsed})</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{statuses.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: 60, gridColumn: '1 / -1' }}>
|
||||
<HardDrive size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
|
||||
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}>暂无 Agent 状态数据</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
584
frontend/src/pages/SettingsPage.tsx
Normal file
584
frontend/src/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,584 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Server,
|
||||
Clock,
|
||||
RefreshCw,
|
||||
Save,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Database,
|
||||
FileJson,
|
||||
FolderOpen,
|
||||
} from 'lucide-react';
|
||||
import { api } from '../lib/api';
|
||||
|
||||
interface Config {
|
||||
apiBaseUrl: string;
|
||||
refreshInterval: number;
|
||||
theme: 'dark' | 'light';
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
const defaultConfig: Config = {
|
||||
apiBaseUrl: 'http://localhost:8000/api',
|
||||
refreshInterval: 10,
|
||||
theme: 'dark',
|
||||
workspace: 'd:\\ccProg\\swarm',
|
||||
};
|
||||
|
||||
export function SettingsPage() {
|
||||
const [config, setConfig] = useState<Config>(defaultConfig);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [backendInfo, setBackendInfo] = useState<{ status: string; version: string } | null>(null);
|
||||
|
||||
// 从 localStorage 加载配置
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('swarm-config');
|
||||
if (stored) {
|
||||
try {
|
||||
setConfig({ ...defaultConfig, ...JSON.parse(stored) });
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 保存配置
|
||||
const handleSave = () => {
|
||||
localStorage.setItem('swarm-config', JSON.stringify(config));
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
|
||||
// 测试连接
|
||||
const handleTestConnection = async () => {
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const health = await api.system.health();
|
||||
setTestResult({ success: true, message: '连接成功' });
|
||||
setBackendInfo({ status: health.status, version: health.version });
|
||||
} catch (err) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: err instanceof Error ? err.message : '连接失败',
|
||||
});
|
||||
setBackendInfo(null);
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<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' }}>
|
||||
配置 API 地址、刷新频率等系统设置
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 360px', gap: 24 }}>
|
||||
{/* Main Settings */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{/* API Configuration */}
|
||||
<div
|
||||
style={{
|
||||
padding: 24,
|
||||
background: 'rgba(17, 24, 39, 0.7)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
background: 'rgba(0, 240, 255, 0.1)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#00f0ff',
|
||||
}}
|
||||
>
|
||||
<Server size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: 0 }}>
|
||||
API 配置
|
||||
</h3>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
|
||||
后端服务连接设置
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
API 基础地址
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<input
|
||||
type="text"
|
||||
value={config.apiBaseUrl}
|
||||
onChange={(e) => setConfig({ ...config, apiBaseUrl: e.target.value })}
|
||||
placeholder="http://localhost:8000/api"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing}
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
background: testing ? 'rgba(255, 255, 255, 0.1)' : '#00f0ff20',
|
||||
border: '1px solid rgba(0, 240, 255, 0.3)',
|
||||
borderRadius: 8,
|
||||
color: '#00f0ff',
|
||||
fontSize: 14,
|
||||
cursor: testing ? 'not-allowed' : 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{testing ? '测试中...' : '测试连接'}
|
||||
</button>
|
||||
</div>
|
||||
{testResult && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
padding: '8px 12px',
|
||||
background: testResult.success ? '#00ff9d20' : '#ff006e20',
|
||||
borderRadius: 6,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{testResult.success ? (
|
||||
<CheckCircle size={16} color="#00ff9d" />
|
||||
) : (
|
||||
<AlertCircle size={16} color="#ff006e" />
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: testResult.success ? '#00ff9d' : '#ff006e',
|
||||
}}
|
||||
>
|
||||
{testResult.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Refresh Settings */}
|
||||
<div
|
||||
style={{
|
||||
padding: 24,
|
||||
background: 'rgba(17, 24, 39, 0.7)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
background: 'rgba(139, 92, 246, 0.1)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#8b5cf6',
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: 0 }}>
|
||||
刷新设置
|
||||
</h3>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
|
||||
数据自动刷新频率
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
自动刷新间隔 (秒)
|
||||
</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="60"
|
||||
value={config.refreshInterval}
|
||||
onChange={(e) =>
|
||||
setConfig({ ...config, refreshInterval: parseInt(e.target.value) })
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
width: 60,
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: 6,
|
||||
color: '#00f0ff',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{config.refreshInterval}s
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '8px 0 0 0' }}>
|
||||
建议值: 仪表盘 10s, 资源监控 5s
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workspace Settings */}
|
||||
<div
|
||||
style={{
|
||||
padding: 24,
|
||||
background: 'rgba(17, 24, 39, 0.7)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
background: 'rgba(0, 255, 157, 0.1)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#00ff9d',
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: 0 }}>
|
||||
工作区配置
|
||||
</h3>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
|
||||
CLI工具工作目录
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
工作区路径 (可选,默认为当前目录)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.workspace}
|
||||
onChange={(e) => setConfig({ ...config, workspace: e.target.value })}
|
||||
placeholder="例如: d:\\projects\\myapp (可选)"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid rgba(0, 240, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
outline: 'none',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
/>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '8px 0 0 0' }}>
|
||||
留空将使用当前目录作为工作区。Agent CLI将在此目录下执行任务。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
padding: '14px 24px',
|
||||
background: saved
|
||||
? '#00ff9d'
|
||||
: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
color: saved ? '#000' : '#000',
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{saved ? (
|
||||
<>
|
||||
<CheckCircle size={18} />
|
||||
已保存
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save size={18} />
|
||||
保存配置
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sidebar - System Info */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{/* Backend Status */}
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
background: 'rgba(17, 24, 39, 0.7)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<h4
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
margin: '0 0 16px 0',
|
||||
}}
|
||||
>
|
||||
后端状态
|
||||
</h4>
|
||||
|
||||
{backendInfo ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '12px 16px',
|
||||
background: '#00ff9d20',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<CheckCircle size={18} color="#00ff9d" />
|
||||
<span style={{ fontSize: 14, color: '#00ff9d' }}>运行正常</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: 12,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
margin: '0 0 4px 0',
|
||||
}}
|
||||
>
|
||||
版本
|
||||
</p>
|
||||
<p style={{ fontSize: 14, color: '#fff', margin: 0 }}>
|
||||
{backendInfo.version}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '12px 16px',
|
||||
background: '#ff950020',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<Clock size={18} color="#ff9500" />
|
||||
<span style={{ fontSize: 14, color: '#ff9500' }}>未连接</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* System Info */}
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
background: 'rgba(17, 24, 39, 0.7)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<h4
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
margin: '0 0 16px 0',
|
||||
}}
|
||||
>
|
||||
系统信息
|
||||
</h4>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 12,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
margin: '0 0 4px 0',
|
||||
}}
|
||||
>
|
||||
前端版本
|
||||
</p>
|
||||
<p style={{ fontSize: 14, color: '#fff', margin: 0 }}>v0.1.0</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: 12,
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
margin: '0 0 4px 0',
|
||||
}}
|
||||
>
|
||||
构建时间
|
||||
</p>
|
||||
<p style={{ fontSize: 14, color: '#fff', margin: 0 }}>
|
||||
{new Date().toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Storage Info */}
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
background: 'rgba(17, 24, 39, 0.7)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<h4
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
margin: '0 0 16px 0',
|
||||
}}
|
||||
>
|
||||
存储位置
|
||||
</h4>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{[
|
||||
{ icon: Database, label: 'Agent 数据', path: '.doc/agents/' },
|
||||
{ icon: FileJson, label: '会议记录', path: '.doc/meetings/' },
|
||||
{ icon: FileJson, label: '工作流', path: '.doc/workflow/' },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.path}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
padding: '10px 12px',
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<item.icon size={14} color="rgba(255, 255, 255, 0.4)" />
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#00f0ff',
|
||||
margin: '2px 0 0 0',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
>
|
||||
{item.path}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
6
frontend/src/pages/index.ts
Normal file
6
frontend/src/pages/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { DashboardPage } from './DashboardPage';
|
||||
export { AgentsPage } from './AgentsPage';
|
||||
export { MeetingsPage } from './MeetingsPage';
|
||||
export { ResourcesPage } from './ResourcesPage';
|
||||
export { SettingsPage } from './SettingsPage';
|
||||
export { WorkflowPage } from './WorkflowPage';
|
||||
72
frontend/src/styles/dashboard.ts
Normal file
72
frontend/src/styles/dashboard.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// Dashboard 页面样式配置
|
||||
|
||||
export const colors = {
|
||||
primary: '#00f0ff',
|
||||
secondary: '#8b5cf6',
|
||||
success: '#00ff9d',
|
||||
warning: '#ff9500',
|
||||
error: '#ff006e',
|
||||
background: {
|
||||
card: 'rgba(17, 24, 39, 0.7)',
|
||||
hover: 'rgba(0, 0, 0, 0.2)',
|
||||
},
|
||||
border: {
|
||||
default: '1px solid rgba(0, 240, 255, 0.1)',
|
||||
hover: '1px solid rgba(0, 240, 255, 0.3)',
|
||||
subtle: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
text: {
|
||||
primary: '#fff',
|
||||
secondary: 'rgba(255, 255, 255, 0.5)',
|
||||
muted: 'rgba(255, 255, 255, 0.4)',
|
||||
subtle: 'rgba(255, 255, 255, 0.6)',
|
||||
},
|
||||
};
|
||||
|
||||
export const statusColors: Record<string, string> = {
|
||||
working: '#00ff9d',
|
||||
idle: '#00f0ff',
|
||||
waiting: '#ff9500',
|
||||
error: '#ff006e',
|
||||
completed: '#00ff9d',
|
||||
in_progress: '#00f0ff',
|
||||
pending: '#ff9500',
|
||||
};
|
||||
|
||||
export const statusBgColors: Record<string, string> = {
|
||||
working: '#00ff9d20',
|
||||
idle: '#00f0ff20',
|
||||
waiting: '#ff950020',
|
||||
error: '#ff006e20',
|
||||
completed: '#00ff9d20',
|
||||
in_progress: '#00f0ff20',
|
||||
pending: '#ff950020',
|
||||
};
|
||||
|
||||
export const layout = {
|
||||
grid: {
|
||||
stats: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
|
||||
gap: 16,
|
||||
},
|
||||
main: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))',
|
||||
gap: 16,
|
||||
},
|
||||
},
|
||||
card: {
|
||||
padding: 20,
|
||||
borderRadius: 12,
|
||||
background: colors.background.card,
|
||||
border: colors.border.default,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
};
|
||||
|
||||
export const transitions = {
|
||||
default: 'all 0.3s ease',
|
||||
};
|
||||
13
frontend/src/styles/index.css
Normal file
13
frontend/src/styles/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@import './swarm.css';
|
||||
|
||||
/* Tailwind 基础样式 */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 额外工具类 */
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
536
frontend/src/styles/swarm.css
Normal file
536
frontend/src/styles/swarm.css
Normal file
@@ -0,0 +1,536 @@
|
||||
/* =============================================
|
||||
SWARM COMMAND CENTER - Sci-Fi Design System
|
||||
============================================= */
|
||||
|
||||
/* CSS 变量定义 */
|
||||
:root {
|
||||
/* 背景色 */
|
||||
--bg-primary: #030712;
|
||||
--bg-secondary: #0a0f1e;
|
||||
--bg-tertiary: #111827;
|
||||
--bg-card: rgba(10, 15, 30, 0.7);
|
||||
|
||||
/* 强调色 */
|
||||
--accent-cyan: #00f0ff;
|
||||
--accent-cyan-dim: rgba(0, 240, 255, 0.15);
|
||||
--accent-amber: #ff9500;
|
||||
--accent-green: #00ff9d;
|
||||
--accent-pink: #ff006e;
|
||||
--accent-purple: #8b5cf6;
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-primary: #e5e7eb;
|
||||
--text-secondary: #9ca3af;
|
||||
--text-dim: #6b7280;
|
||||
|
||||
/* 边框颜色 */
|
||||
--border-dim: rgba(0, 240, 255, 0.1);
|
||||
--border-bright: rgba(0, 240, 255, 0.3);
|
||||
|
||||
/* 渐变 */
|
||||
--gradient-primary: linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%);
|
||||
--gradient-success: linear-gradient(135deg, #00ff9d 0%, #00f0ff 100%);
|
||||
--gradient-warm: linear-gradient(135deg, #ff9500 0%, #ff006e 100%);
|
||||
}
|
||||
|
||||
/* 基础重置 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
动画定义
|
||||
============================================= */
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 10px currentColor;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
box-shadow: 0 0 5px currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-fast {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
box-shadow: 0 0 3px currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.3); }
|
||||
}
|
||||
|
||||
@keyframes rotate-slow {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes scanline {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 0 50px; }
|
||||
}
|
||||
|
||||
@keyframes message-slide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes line-flow {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
75% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
@keyframes border-glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 10px rgba(255, 149, 0, 0.3),
|
||||
inset 0 0 10px rgba(255, 149, 0, 0.05);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 25px rgba(255, 149, 0, 0.6),
|
||||
inset 0 0 20px rgba(255, 149, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes status-working {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 6px #00ff9d;
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 12px #00ff9d;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes grid-drift {
|
||||
from { transform: translate(0, 0); }
|
||||
to { transform: translate(50px, 50px); }
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: scale(4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
工具类
|
||||
============================================= */
|
||||
|
||||
.animate-pulse-glow { animation: pulse-glow 2s infinite; }
|
||||
.animate-pulse-fast { animation: pulse-fast 1.5s infinite; }
|
||||
.animate-scale-pulse { animation: scale-pulse 1.5s infinite; }
|
||||
.animate-rotate-slow { animation: rotate-slow 4s linear infinite; }
|
||||
.animate-message-slide { animation: message-slide 0.3s ease forwards; }
|
||||
.animate-shake { animation: shake 0.5s ease; }
|
||||
.animate-border-glow { animation: border-glow 2s infinite; }
|
||||
.animate-status-working { animation: status-working 2s infinite; }
|
||||
.animate-fade-in-up { animation: fade-in-up 0.4s ease forwards; }
|
||||
|
||||
/* =============================================
|
||||
背景效果
|
||||
============================================= */
|
||||
|
||||
.bg-grid {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bg-scanline {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0, 240, 255, 0.01) 2px,
|
||||
rgba(0, 240, 255, 0.01) 4px
|
||||
);
|
||||
animation: scanline 20s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
玻璃卡片
|
||||
============================================= */
|
||||
|
||||
.glass-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glass-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 240, 255, 0.3), transparent);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
border-color: var(--border-bright);
|
||||
box-shadow: 0 0 30px rgba(0, 240, 255, 0.1);
|
||||
}
|
||||
|
||||
.glass-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
卡片标题
|
||||
============================================= */
|
||||
|
||||
.card-title {
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
滚动条样式
|
||||
============================================= */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 240, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 240, 255, 0.4);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
进度条
|
||||
============================================= */
|
||||
|
||||
.progress-bar-cyan {
|
||||
background: linear-gradient(90deg, #00f0ff, #8b5cf6);
|
||||
}
|
||||
|
||||
.progress-bar-green {
|
||||
background: linear-gradient(90deg, #00ff9d, #00f0ff);
|
||||
}
|
||||
|
||||
.progress-bar-amber {
|
||||
background: linear-gradient(90deg, #ff9500, #ff006e);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
状态徽章
|
||||
============================================= */
|
||||
|
||||
.badge {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
智能体头像
|
||||
============================================= */
|
||||
|
||||
.agent-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
时间轴
|
||||
============================================= */
|
||||
|
||||
.timeline-line {
|
||||
height: 4px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-line-flow::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 240, 255, 0.6), transparent);
|
||||
animation: line-flow 2s linear infinite;
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
快速标签
|
||||
============================================= */
|
||||
|
||||
.quick-tag {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(0, 240, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
padding: 5px 14px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.quick-tag:hover {
|
||||
border-color: rgba(0, 240, 255, 0.4);
|
||||
color: var(--accent-cyan);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
按钮
|
||||
============================================= */
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gradient-primary);
|
||||
color: #030712;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
box-shadow: 0 0 30px rgba(0, 240, 255, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(0, 240, 255, 0.1);
|
||||
color: var(--accent-cyan);
|
||||
border: 1px solid rgba(0, 240, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 11px 22px;
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 20px rgba(0, 240, 255, 0.2);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: rgba(0, 240, 255, 0.1);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
字体类
|
||||
============================================= */
|
||||
|
||||
.font-orbitron { font-family: 'Orbitron', monospace; }
|
||||
.font-mono-code { font-family: 'JetBrains Mono', monospace; }
|
||||
.font-rajdhani { font-family: 'Rajdhani', sans-serif; }
|
||||
.font-noto { font-family: 'Noto Sans SC', sans-serif; }
|
||||
|
||||
/* =============================================
|
||||
文本颜色
|
||||
============================================= */
|
||||
|
||||
.text-neon-cyan {
|
||||
color: var(--accent-cyan);
|
||||
text-shadow: 0 0 10px rgba(0, 240, 255, 0.5);
|
||||
}
|
||||
|
||||
.text-neon-green {
|
||||
color: var(--accent-green);
|
||||
text-shadow: 0 0 10px rgba(0, 255, 157, 0.4);
|
||||
}
|
||||
|
||||
.text-neon-amber {
|
||||
color: var(--accent-amber);
|
||||
text-shadow: 0 0 10px rgba(255, 149, 0, 0.4);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
Bento Grid 布局
|
||||
============================================= */
|
||||
|
||||
.bento-grid-main {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bento-grid-row3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bento-agent { grid-column: 1 / 3; grid-row: 1 / 3; min-height: 420px; }
|
||||
.bento-discuss { grid-column: 3 / 5; grid-row: 1 / 3; min-height: 420px; }
|
||||
.bento-meeting { grid-column: 5 / 7; grid-row: 1 / 2; }
|
||||
.bento-resource { grid-column: 5 / 7; grid-row: 2 / 3; }
|
||||
|
||||
.bento-stats { grid-column: 1 / 2; }
|
||||
.bento-workflow { grid-column: 2 / 3; }
|
||||
.bento-consensus { grid-column: 3 / 5; }
|
||||
.bento-barrier { grid-column: 5 / 7; }
|
||||
|
||||
/* 响应式:平板 */
|
||||
@media (max-width: 1400px) {
|
||||
.bento-grid-main {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
.bento-agent { grid-column: 1 / 3; grid-row: 1 / 3; }
|
||||
.bento-discuss { grid-column: 3 / 5; grid-row: 1 / 3; }
|
||||
.bento-meeting { grid-column: 1 / 3; grid-row: auto; }
|
||||
.bento-resource { grid-column: 3 / 5; grid-row: auto; }
|
||||
|
||||
.bento-grid-row3 { grid-template-columns: repeat(4, 1fr); }
|
||||
.bento-stats { grid-column: 1 / 2; }
|
||||
.bento-workflow { grid-column: 2 / 3; }
|
||||
.bento-consensus { grid-column: 3 / 5; }
|
||||
.bento-barrier { grid-column: 1 / -1; }
|
||||
}
|
||||
|
||||
/* 响应式:移动端 */
|
||||
@media (max-width: 768px) {
|
||||
.bento-grid-main,
|
||||
.bento-grid-row3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.bento-agent, .bento-discuss, .bento-meeting, .bento-resource,
|
||||
.bento-stats, .bento-workflow, .bento-consensus, .bento-barrier {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: auto;
|
||||
min-height: 280px;
|
||||
}
|
||||
.bento-agent, .bento-discuss {
|
||||
min-height: 360px;
|
||||
}
|
||||
}
|
||||
138
frontend/src/types/index.ts
Normal file
138
frontend/src/types/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
// 类型定义
|
||||
|
||||
// Agent 相关
|
||||
export interface Agent {
|
||||
agent_id: string;
|
||||
name: string;
|
||||
role: 'architect' | 'pm' | 'developer' | 'qa' | 'reviewer' | 'human';
|
||||
model: string;
|
||||
status: 'idle' | 'working' | 'waiting' | 'error';
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AgentState {
|
||||
agent_id: string;
|
||||
current_task: string;
|
||||
progress: number;
|
||||
working_files: string[];
|
||||
last_update: string;
|
||||
}
|
||||
|
||||
// 文件锁
|
||||
export interface FileLock {
|
||||
file_path: string;
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
acquired_at: string;
|
||||
elapsed_display: string;
|
||||
}
|
||||
|
||||
// 心跳
|
||||
export interface Heartbeat {
|
||||
agent_id: string;
|
||||
status: 'working' | 'waiting' | 'idle' | 'error';
|
||||
current_task: string;
|
||||
progress: number;
|
||||
elapsed_display: string;
|
||||
is_timeout: boolean;
|
||||
}
|
||||
|
||||
// 会议
|
||||
export interface Meeting {
|
||||
meeting_id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
status: 'pending' | 'waiting' | 'in_progress' | 'completed';
|
||||
attendees: string[];
|
||||
steps: MeetingStep[];
|
||||
discussions: Discussion[];
|
||||
progress_summary: string;
|
||||
consensus: string;
|
||||
}
|
||||
|
||||
export interface MeetingStep {
|
||||
step_id: string;
|
||||
label: string;
|
||||
status: 'pending' | 'active' | 'completed';
|
||||
}
|
||||
|
||||
export interface Discussion {
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
step: string;
|
||||
}
|
||||
|
||||
export interface MeetingQueue {
|
||||
meeting_id: string;
|
||||
title: string;
|
||||
status: 'waiting' | 'started' | 'ended';
|
||||
expected_attendees: string[];
|
||||
arrived_attendees: string[];
|
||||
missing_attendees: string[];
|
||||
progress: string;
|
||||
is_ready: boolean;
|
||||
}
|
||||
|
||||
// 工作流
|
||||
export interface Workflow {
|
||||
workflow_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: 'pending' | 'in_progress' | 'completed';
|
||||
progress: string;
|
||||
meetings: WorkflowMeeting[];
|
||||
}
|
||||
|
||||
export interface WorkflowMeeting {
|
||||
meeting_id: string;
|
||||
title: string;
|
||||
node_type: 'meeting' | 'execution';
|
||||
attendees: string[];
|
||||
depends_on: string[];
|
||||
completed: boolean;
|
||||
on_failure?: string;
|
||||
progress?: string;
|
||||
}
|
||||
|
||||
// 资源状态
|
||||
export interface AgentResourceStatus {
|
||||
agent_id: string;
|
||||
info: {
|
||||
name: string;
|
||||
role: string;
|
||||
model: string;
|
||||
};
|
||||
heartbeat: {
|
||||
status: string;
|
||||
current_task: string;
|
||||
progress: number;
|
||||
elapsed: string;
|
||||
};
|
||||
locks: Array<{
|
||||
file: string;
|
||||
elapsed: string;
|
||||
}>;
|
||||
state: {
|
||||
task: string;
|
||||
progress: number;
|
||||
working_files: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// API 响应
|
||||
export interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 配置
|
||||
export interface AppConfig {
|
||||
apiBaseUrl: string;
|
||||
refreshInterval: number;
|
||||
theme: 'dark' | 'light';
|
||||
}
|
||||
Reference in New Issue
Block a user