chore: initial commit for Pretor v0.1.0-alpha
正式发布 Pretor 平台的首个 alpha 版本。本项目旨在构建一个基于分布式架构的多智能体协同工作流水线。 核心功能实现: 1. 建立基于 BaseIndividual 的动态插件加载机制。 2. 实现三类核心 worker_individual 子个体。 3. 集成 Ray 框架支持分布式集群调度。 4. 基于 PostgreSQL 的全量持久化存储方案。 5. 提供完整的 FastAPI 后端与 React 前端交互界面。
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Server, Box, Cpu, HardDrive, List, MessageCircle } from 'lucide-react';
|
||||
import { useClusterState } from '../../hooks/useClusterState';
|
||||
import apiClient from '../../api/client';
|
||||
import type { Workflow } from '../../types';
|
||||
|
||||
interface LeftPanelProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
selectedWorkflow: string | null;
|
||||
setSelectedWorkflow: (id: string | null) => void;
|
||||
}
|
||||
|
||||
export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelectedWorkflow }: LeftPanelProps) {
|
||||
const { nodes } = useClusterState();
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const [loadingWorkflows, setLoadingWorkflows] = useState(false);
|
||||
|
||||
const totalNodes = nodes.length;
|
||||
const aliveNodes = nodes.filter(n => n.alive).length;
|
||||
|
||||
let totalCpu = 0;
|
||||
let usedCpu = 0;
|
||||
let totalMemory = 0;
|
||||
let usedMemory = 0;
|
||||
|
||||
nodes.forEach(node => {
|
||||
const nodeTotalCpu = node.resources?.CPU || 0;
|
||||
const nodeRemainingCpu = node.remaining?.CPU || 0;
|
||||
totalCpu += nodeTotalCpu;
|
||||
usedCpu += (nodeTotalCpu - nodeRemainingCpu);
|
||||
|
||||
const nodeTotalMem = node.resources?.memory || 0;
|
||||
const nodeRemainingMem = node.remaining?.memory || 0;
|
||||
totalMemory += nodeTotalMem;
|
||||
usedMemory += (nodeTotalMem - nodeRemainingMem);
|
||||
});
|
||||
|
||||
const cpuPercent = totalCpu > 0 ? (usedCpu / totalCpu) * 100 : 0;
|
||||
const memPercent = totalMemory > 0 ? (usedMemory / totalMemory) * 100 : 0;
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId: ReturnType<typeof setInterval>;
|
||||
|
||||
const fetchWorkflows = async (isInitial = false) => {
|
||||
if (isInitial) setLoadingWorkflows(true);
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/workflow/list');
|
||||
// Fallback parsing just in case it returns an object or array
|
||||
const data = response.data;
|
||||
let parsedWorkflows: Workflow[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
parsedWorkflows = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
// Suppose backend sends { workflows: [...] }
|
||||
parsedWorkflows = data.workflows || Object.values(data);
|
||||
}
|
||||
setWorkflows(parsedWorkflows);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch workflows", error);
|
||||
setWorkflows([]);
|
||||
} finally {
|
||||
if (isInitial) setLoadingWorkflows(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (activeTab === 'workflows') {
|
||||
fetchWorkflows(true);
|
||||
intervalId = setInterval(() => fetchWorkflows(false), 2000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
};
|
||||
}, [activeTab]);
|
||||
|
||||
return (
|
||||
<div className="w-72 bg-white border-r border-slate-200 flex flex-col z-0 shrink-0">
|
||||
{/* Top: Cluster Status */}
|
||||
<div className="h-1/3 p-4 border-b border-slate-100 flex flex-col">
|
||||
<h2 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-4 flex items-center">
|
||||
<Server size={16} className="mr-2" />
|
||||
Cluster Status
|
||||
</h2>
|
||||
<div className="space-y-4 flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center text-slate-600">
|
||||
<Box size={16} className="mr-2 text-blue-500" />
|
||||
<span className="text-sm">Active Nodes</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-800">{aliveNodes} / {totalNodes || 0}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center text-slate-600">
|
||||
<Cpu size={16} className="mr-2 text-indigo-500" />
|
||||
<span className="text-sm">Cluster CPU</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-800">{cpuPercent.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 rounded-full h-1.5">
|
||||
<div className="bg-indigo-500 h-1.5 rounded-full" style={{ width: `${cpuPercent}%` }}></div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center text-slate-600">
|
||||
<HardDrive size={16} className="mr-2 text-green-500" />
|
||||
<span className="text-sm">Cluster RAM</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-800">
|
||||
{(totalMemory > 0 ? usedMemory / (1024 ** 3) : 0).toFixed(1)} GB
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 rounded-full h-1.5">
|
||||
<div className="bg-green-500 h-1.5 rounded-full" style={{ width: `${memPercent}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom: Tabs for Workflows & Basic Chats */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex border-b border-slate-100">
|
||||
<button
|
||||
onClick={() => setActiveTab('chats')}
|
||||
className={`flex-1 py-3 text-xs font-medium text-center uppercase tracking-wider transition-colors ${activeTab === 'chats' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-slate-500 hover:bg-slate-50'}`}
|
||||
>
|
||||
<MessageCircle size={14} className="inline mr-1.5 -mt-0.5" />
|
||||
Chats
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('workflows')}
|
||||
className={`flex-1 py-3 text-xs font-medium text-center uppercase tracking-wider transition-colors ${activeTab === 'workflows' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-slate-500 hover:bg-slate-50'}`}
|
||||
>
|
||||
<List size={14} className="inline mr-1.5 -mt-0.5" />
|
||||
Workflows
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-4 overflow-y-auto">
|
||||
{activeTab === 'workflows' && (
|
||||
<div className="space-y-2">
|
||||
{loadingWorkflows ? (
|
||||
<div className="text-center text-slate-400 text-sm py-4">Loading workflows...</div>
|
||||
) : workflows.length === 0 ? (
|
||||
<div className="text-center text-slate-400 text-sm py-4">暂无工作流</div>
|
||||
) : (
|
||||
workflows.map((wf) => (
|
||||
<div
|
||||
key={wf.event_id}
|
||||
onClick={() => setSelectedWorkflow(wf.event_id)}
|
||||
className={`p-3 rounded-lg border cursor-pointer transition-all ${selectedWorkflow === wf.event_id ? 'border-blue-200 bg-blue-50 shadow-sm' : 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className={`font-medium text-sm ${selectedWorkflow === wf.event_id ? 'text-blue-700' : 'text-slate-700'}`}>{wf.workflow_title || 'Unnamed Workflow'}</span>
|
||||
<span className={`flex h-2 w-2 rounded-full ${wf.status === 'llm_working' || wf.status === 'tool_working' ? 'bg-green-400 animate-pulse' : wf.status === 'failed' ? 'bg-red-400' : 'bg-slate-300'}`}></span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 font-mono line-clamp-1">ID: {wf.event_id}</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'chats' && (
|
||||
<div className="space-y-2">
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user