完整实现 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:
Claude Code
2026-03-09 17:32:11 +08:00
commit dc398d7c7b
118 changed files with 23120 additions and 0 deletions

32
frontend/src/App.tsx Normal file
View 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} />;
}

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

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

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

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

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

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

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

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

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

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

View 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}`,
}}
/>
);
}

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,2 @@
export { AppLayout } from './AppLayout';
export { Sidebar } from './Sidebar';

428
frontend/src/lib/api.ts Normal file
View 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
View 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>,
);

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

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

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

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

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

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

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

View 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',
};

View File

@@ -0,0 +1,13 @@
@import './swarm.css';
/* Tailwind 基础样式 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 额外工具类 */
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

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