chore(release): v0.1.1-alpha

##前端美化和bug修复
#### 💄 美化
- **前端美化**:对于整个前端效果进行了重新设计,现在的前端看起来会更立体。

#### 🐛 修复
- **前端演示**:修复了前端展示workflow列表的bug,但是workflow的具体条目显示由于序列化导致仍然有问题。 
- **密钥修复**:对于secret_key现在在使用默认情况时,会强制生成一个安全的密钥。
This commit is contained in:
2026-05-04 16:38:21 +08:00
committed by GitHub
parent d84212f780
commit d30c7e37a6
92 changed files with 2449 additions and 863 deletions
+119 -94
View File
@@ -1,44 +1,32 @@
import { useState, useEffect } from 'react';
import { Server, Box, Cpu, HardDrive, List, MessageCircle } from 'lucide-react';
import { useClusterState } from '../../hooks/useClusterState';
import { Plus, Trash2 } from 'lucide-react';
import apiClient from '../../api/client';
import type { Workflow } from '../../types';
import type { ChatSession } from '../../App';
interface LeftPanelProps {
activeTab: string;
setActiveTab: (tab: string) => void;
selectedWorkflow: string | null;
setSelectedWorkflow: (id: string | null) => void;
// Hoisted state props (optional, since this panel is used for workflows too)
chatSessions?: ChatSession[];
setChatSessions?: React.Dispatch<React.SetStateAction<ChatSession[]>>;
activeSessionId?: string | null;
setActiveSessionId?: React.Dispatch<React.SetStateAction<string | null>>;
}
export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelectedWorkflow }: LeftPanelProps) {
const { nodes } = useClusterState();
export function LeftPanel({
activeTab,
selectedWorkflow,
setSelectedWorkflow,
chatSessions,
setChatSessions,
activeSessionId,
setActiveSessionId,
}: LeftPanelProps) {
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>;
@@ -46,18 +34,16 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
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);
console.error('Failed to fetch workflows', error);
setWorkflows([]);
} finally {
if (isInitial) setLoadingWorkflows(false);
@@ -74,69 +60,56 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
};
}, [activeTab]);
const handleNewChat = () => {
if (!setChatSessions || !setActiveSessionId) return;
const newSession: ChatSession = {
id: Date.now().toString(),
title: 'New Chat',
messages: [
{
id: Date.now().toString(),
role: 'assistant',
content: 'Hello! I am Pretor Assistant. How can I help you today?',
timestamp: Date.now(),
},
],
updatedAt: Date.now(),
};
setChatSessions((prev) => [newSession, ...prev]);
setActiveSessionId(newSession.id);
};
const handleDeleteChat = (e: React.MouseEvent, id: string) => {
e.stopPropagation();
if (!setChatSessions || !setActiveSessionId || !chatSessions) return;
const updated = chatSessions.filter((s) => s.id !== id);
setChatSessions(updated);
if (activeSessionId === id) {
setActiveSessionId(updated.length > 0 ? updated[0].id : null);
}
};
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="w-72 bg-white border-r border-slate-100 flex flex-col z-0 shrink-0">
{/* Bottom: Tab Selection */}
<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 className="flex items-center justify-between p-3 border-b border-slate-100 bg-slate-50">
<span className="text-sm font-semibold text-slate-600 uppercase tracking-wider">
{activeTab === 'chats' ? 'Chat History' : 'Workflows'}
</span>
{activeTab === 'chats' && (
<button
onClick={handleNewChat}
className="p-1.5 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 transition-colors"
title="New Chat"
>
<Plus size={16} />
</button>
)}
</div>
<div className="flex-1 p-4 overflow-y-auto">
<div className="flex-1 p-3 overflow-y-auto">
{activeTab === 'workflows' && (
<div className="space-y-2">
{loadingWorkflows ? (
@@ -148,11 +121,29 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
<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'}`}
className={`p-3 rounded-lg border cursor-pointer transition-all ${
selectedWorkflow === wf.event_id
? 'border-blue-300 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>
<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>
@@ -160,8 +151,42 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
)}
</div>
)}
{activeTab === 'chats' && (
{activeTab === 'chats' && chatSessions && (
<div className="space-y-2">
{chatSessions.length === 0 ? (
<div className="text-center text-slate-400 text-sm py-8">
No chat history.<br/>Click + to start a new chat.
</div>
) : (
chatSessions.map((session) => (
<div
key={session.id}
onClick={() => setActiveSessionId?.(session.id)}
className={`group flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-all ${
activeSessionId === session.id
? 'border-blue-300 bg-blue-50 shadow-sm'
: 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'
}`}
>
<div className="flex-1 min-w-0 mr-2">
<h3 className={`font-medium text-sm truncate ${
activeSessionId === session.id ? 'text-blue-700' : 'text-slate-700'
}`}>
{session.title}
</h3>
<p className="text-xs text-slate-400 mt-1">
{new Date(session.updatedAt).toLocaleDateString()}
</p>
</div>
<button
onClick={(e) => handleDeleteChat(e, session.id)}
className="text-slate-400 opacity-0 group-hover:opacity-100 hover:text-red-500 transition-all"
>
<Trash2 size={16} />
</button>
</div>
))
)}
</div>
)}
</div>