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
+232 -2
View File
@@ -8,6 +8,7 @@
"name": "pretor-dashboard",
"version": "0.0.0",
"dependencies": {
"@xyflow/react": "^12.10.2",
"axios": "^1.15.1",
"lucide-react": "^1.8.0",
"react": "^19.2.4",
@@ -1153,6 +1154,55 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"license": "MIT"
},
"node_modules/@types/d3-transition": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"license": "MIT",
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1181,7 +1231,7 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1518,6 +1568,38 @@
}
}
},
"node_modules/@xyflow/react": {
"version": "12.10.2",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz",
"integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.76",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.76",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz",
"integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -1724,6 +1806,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"license": "MIT"
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1789,9 +1877,114 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -3461,6 +3654,15 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
@@ -3607,6 +3809,34 @@
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
}
}
}
+1
View File
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@xyflow/react": "^12.10.2",
"axios": "^1.15.1",
"lucide-react": "^1.8.0",
"react": "^19.2.4",
+115 -34
View File
@@ -1,25 +1,58 @@
import { useState, useEffect } from 'react';
import { Sidebar } from './components/Layout/Sidebar';
import { TopBar } from './components/Layout/TopBar';
import { CollapsibleSidebar } from './components/Layout/CollapsibleSidebar';
import { SettingsLayout } from './components/Settings/SettingsLayout';
import { AgentLayout } from './components/Agent/AgentLayout';
import { ResourceLayout } from './components/Resource/ResourceLayout';
import { PluginLayout } from './components/Plugin/PluginLayout'; // Will rename to PluginLayout soon
import { LeftPanel } from './components/Chat/LeftPanel';
import { ChatPanel } from './components/Chat/ChatPanel';
import { RightPanel } from './components/Chat/RightPanel';
import { WorkflowListView } from './components/Chat/WorkflowListView';
import { AuthPage } from './components/Auth/AuthPage';
// For Chat Module State Persistence
export interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: number;
}
export interface ChatSession {
id: string;
title: string;
messages: Message[];
updatedAt: number;
}
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [activeTab, setActiveTab] = useState('chats'); // For LeftPanel
const [currentView, setCurrentView] = useState('dashboard'); // 'dashboard', 'settings', 'agent', 'resource'
const [settingsTab, setSettingsTab] = useState('users'); // For SettingsLayout
const [agentTab, setAgentTab] = useState('worker'); // For AgentLayout
const [resourceTab, setResourceTab] = useState('skill'); // For ResourceLayout
// Layout State
const [mode, setMode] = useState<'work' | 'agent'>('work');
const [showSettings, setShowSettings] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
// Module Sub-navigation States
// Work Mode
const [workTab, setWorkTab] = useState<'chat' | 'workflow'>('chat');
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null);
// Agent Mode
const [agentTab, setAgentTab] = useState<'plugin' | 'agents'>('plugin');
// Settings Sub-tab
const [settingsTab, setSettingsTab] = useState('users');
// Inner Agent Tab (temporary until full Agent layout rewrite)
const [innerAgentTab, setInnerAgentTab] = useState('worker');
const [resourceTab, setResourceTab] = useState('skill');
// Chat State Hoisted for Persistence
const [chatSessions, setChatSessions] = useState<ChatSession[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
useEffect(() => {
// Check if token exists in localStorage on mount
const token = localStorage.getItem('token');
if (token) {
setIsAuthenticated(true);
@@ -31,37 +64,85 @@ function App() {
}
return (
<div className="flex h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
<div className="flex flex-col h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
{/* 1. Top Bar */}
<TopBar
mode={mode}
setMode={setMode}
showSettings={showSettings}
setShowSettings={setShowSettings}
/>
{/* 1. Sidebar (Leftmost) */}
<Sidebar currentView={currentView} setCurrentView={setCurrentView} />
{/* 2. Main Content Area */}
<div className="flex flex-1 overflow-hidden relative">
{showSettings ? (
<SettingsLayout settingsTab={settingsTab} setSettingsTab={setSettingsTab} />
) : (
<>
{/* Collapsible Main Sidebar */}
<CollapsibleSidebar
mode={mode}
isOpen={isSidebarOpen}
setIsOpen={setIsSidebarOpen}
workTab={workTab}
setWorkTab={setWorkTab}
agentTab={agentTab}
setAgentTab={setAgentTab}
/>
{/* Main Content Area depending on view */}
{currentView === 'agent' ? (
<AgentLayout agentTab={agentTab} setAgentTab={setAgentTab} />
) : currentView === 'resource' ? (
<ResourceLayout resourceTab={resourceTab} setResourceTab={setResourceTab} />
) : currentView === 'dashboard' ? (
<>
{/* 2. Left Panel - Cluster Status & Workflows/Chats */}
<LeftPanel
activeTab={activeTab}
setActiveTab={setActiveTab}
selectedWorkflow={selectedWorkflow}
setSelectedWorkflow={setSelectedWorkflow}
/>
{/* Dynamic View based on Mode and Tab */}
<div className="flex-1 flex overflow-hidden">
{mode === 'work' && workTab === 'chat' && (
<div className="flex-1 p-6 flex overflow-hidden">
<div className="flex-1 flex bg-white rounded-3xl shadow-md border border-slate-200 overflow-hidden relative">
<LeftPanel
activeTab="chats"
selectedWorkflow={null}
setSelectedWorkflow={() => {}}
// Pass hoisted state down
chatSessions={chatSessions}
setChatSessions={setChatSessions}
activeSessionId={activeSessionId}
setActiveSessionId={setActiveSessionId}
/>
<ChatPanel
chatSessions={chatSessions}
setChatSessions={setChatSessions}
activeSessionId={activeSessionId}
setActiveSessionId={setActiveSessionId}
/>
</div>
</div>
)}
{/* 3. Middle Panel - AI Chat */}
<ChatPanel />
{mode === 'work' && workTab === 'workflow' && (
<>
{selectedWorkflow ? (
<>
<LeftPanel
activeTab="workflows"
selectedWorkflow={selectedWorkflow}
setSelectedWorkflow={setSelectedWorkflow}
/>
<RightPanel selectedWorkflow={selectedWorkflow} />
</>
) : (
<WorkflowListView onSelectWorkflow={setSelectedWorkflow} />
)}
</>
)}
{/* 4. Right Panel - Workflow Execution Status (Only show when viewing workflows) */}
{activeTab === 'workflows' && <RightPanel selectedWorkflow={selectedWorkflow} />}
</>
) : (
/* Settings View */
<SettingsLayout settingsTab={settingsTab} setSettingsTab={setSettingsTab} />
)}
{mode === 'agent' && agentTab === 'agents' && (
<AgentLayout agentTab={innerAgentTab} setAgentTab={setInnerAgentTab} />
)}
{mode === 'agent' && agentTab === 'plugin' && (
<PluginLayout resourceTab={resourceTab} setResourceTab={setResourceTab} />
)}
</div>
</>
)}
</div>
</div>
);
}
+25 -29
View File
@@ -1,4 +1,3 @@
import { Bot, Key } from 'lucide-react';
import { ProvidersSettings } from './ProvidersSettings';
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
@@ -9,35 +8,32 @@ interface AgentLayoutProps {
export function AgentLayout({ agentTab, setAgentTab }: AgentLayoutProps) {
return (
<div className="flex-1 flex bg-slate-50 overflow-hidden">
{/* Agent Inner Sidebar */}
<div className="w-64 bg-white border-r border-slate-200 flex flex-col z-0">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-800">Agents</h2>
</div>
<div className="flex-1 p-4 space-y-2 overflow-y-auto">
<button
onClick={() => setAgentTab('worker')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${agentTab === 'worker' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<Bot size={18} className="mr-3" />
Individual
</button>
<button
onClick={() => setAgentTab('providers')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${agentTab === 'providers' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<Key size={18} className="mr-3" />
Provider Management
</button>
</div>
</div>
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
{/* Top Tabs for Agent Module */}
<div className="h-14 border-b border-slate-200 bg-white flex items-center px-6 shadow-sm z-10 shrink-0 space-x-6">
<button
onClick={() => setAgentTab('worker')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
agentTab === 'worker' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
}`}
>
Individual
</button>
<button
onClick={() => setAgentTab('providers')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
agentTab === 'providers' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
}`}
>
Provider Management
</button>
</div>
{/* Agent Main Content */}
<div className="flex-1 overflow-y-auto p-8">
{agentTab === 'worker' && <WorkerIndividualSettings />}
{agentTab === 'providers' && <ProvidersSettings />}
</div>
{/* Main Content */}
<div className="flex-1 overflow-y-auto p-8">
{agentTab === 'worker' && <WorkerIndividualSettings />}
{agentTab === 'providers' && <ProvidersSettings />}
</div>
</div>
);
}
@@ -167,7 +167,7 @@ export function WorkerIndividualSettings() {
</div>
<button
onClick={handleAddNew}
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Plus size={16} className="mr-2" />
Add Worker
@@ -203,7 +203,7 @@ export function WorkerIndividualSettings() {
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
</td>
<td className="p-4 text-right space-x-2">
<button onClick={() => handleEdit(w)} className="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors" title="Edit">
<button onClick={() => handleEdit(w)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Edit">
<Edit2 size={16} />
</button>
</td>
@@ -219,7 +219,7 @@ export function WorkerIndividualSettings() {
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
</td>
<td className="p-4 text-right space-x-2">
<button onClick={() => handleEdit(w)} className="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors" title="Edit">
<button onClick={() => handleEdit(w)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Edit">
<Edit2 size={16} />
</button>
<button onClick={() => handleDelete(w.agent_id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Delete">
@@ -256,7 +256,7 @@ export function WorkerIndividualSettings() {
required
value={editData.agent_name || ''}
onChange={(e) => setEditData({...editData, agent_name: e.target.value})}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
disabled={(editData as any).is_system}
/>
</div>
@@ -265,7 +265,7 @@ export function WorkerIndividualSettings() {
<select
value={editData.agent_type || 'ordinary_individual'}
onChange={(e) => setEditData({...editData, agent_type: e.target.value})}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
disabled={(editData as any).is_system}
>
<option value="ordinary_individual">Ordinary Individual</option>
@@ -285,7 +285,7 @@ export function WorkerIndividualSettings() {
value={editData.provider_title || ''}
onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})}
required
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="" disabled>Select Provider</option>
{providers.map((p) => (
@@ -303,7 +303,7 @@ export function WorkerIndividualSettings() {
value={editData.model_id || ''}
onChange={(e) => setEditData({...editData, model_id: e.target.value})}
required
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="" disabled>Select a model</option>
{models.map(m => <option key={m} value={m}>{m}</option>)}
@@ -321,7 +321,7 @@ export function WorkerIndividualSettings() {
value={editData.description || ''}
onChange={(e) => setEditData({...editData, description: e.target.value})}
rows={2}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
@@ -331,7 +331,7 @@ export function WorkerIndividualSettings() {
value={editData.system_prompt || ''}
onChange={(e) => setEditData({...editData, system_prompt: e.target.value})}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
/>
</div>
@@ -342,7 +342,7 @@ export function WorkerIndividualSettings() {
value={editData.output_template || '{}'}
onChange={(e) => setEditData({...editData, output_template: e.target.value})}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
/>
</div>
<div>
@@ -359,7 +359,7 @@ export function WorkerIndividualSettings() {
const newSkill = val ? { [val]: [] } : {};
setEditData({...editData, bound_skill: JSON.stringify(newSkill)});
}}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
disabled={editData.agent_type !== 'skill_individual'}
>
<option value="">No Skill Bound</option>
@@ -376,7 +376,7 @@ export function WorkerIndividualSettings() {
value={editData.workspace || '[]'}
onChange={(e) => setEditData({...editData, workspace: e.target.value})}
rows={2}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
/>
</div>
</>
@@ -407,7 +407,7 @@ export function WorkerIndividualSettings() {
}}
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
isSelected
? 'bg-indigo-100 text-indigo-700 border border-indigo-200'
? 'bg-blue-100 text-blue-700 border border-blue-200'
: 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100'
}`}
>
@@ -437,7 +437,7 @@ export function WorkerIndividualSettings() {
</button>
<button
type="submit"
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Save size={16} className="mr-2" />
Save Worker
+153 -102
View File
@@ -1,31 +1,47 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { MessageSquare, Activity, Terminal, ChevronRight, Plus } from 'lucide-react';
import apiClient from '../../api/client';
import type { ChatSession, Message } from '../../App';
interface ChatMessage {
id: string;
sender: 'user' | 'ai';
text: string;
timestamp: Date;
eventId?: string;
interface ChatPanelProps {
chatSessions: ChatSession[];
setChatSessions: React.Dispatch<React.SetStateAction<ChatSession[]>>;
activeSessionId: string | null;
setActiveSessionId: React.Dispatch<React.SetStateAction<string | null>>;
}
export function ChatPanel() {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
sender: 'ai',
text: "Hello! I am Pretor Assistant. How can I help you today?",
timestamp: new Date()
}
]);
export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setActiveSessionId }: ChatPanelProps) {
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const fileInputRef = React.useRef<HTMLInputElement>(null);
const [mode, setMode] = useState<'chat' | 'deploy'>('chat');
const fileInputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const activeSession = chatSessions.find((s) => s.id === activeSessionId) || null;
const messages = activeSession ? activeSession.messages : [];
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const updateSessionMessages = (newMessages: Message[]) => {
if (!activeSessionId) return;
setChatSessions((prev) =>
prev.map((s) =>
s.id === activeSessionId
? { ...s, messages: newMessages, updatedAt: Date.now() }
: s
)
);
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file || !activeSessionId) return;
const formData = new FormData();
formData.append('file', file);
@@ -33,26 +49,24 @@ export function ChatPanel() {
setLoading(true);
try {
const response = await apiClient.post('/api/v1/adapter/client/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
headers: { 'Content-Type': 'multipart/form-data' },
});
const aiMessage: ChatMessage = {
const aiMessage: Message = {
id: Date.now().toString(),
sender: 'ai',
text: `已上传文件: ${response.data.filename}`,
timestamp: new Date()
role: 'assistant',
content: `已上传文件: ${response.data.filename}`,
timestamp: Date.now(),
};
setMessages(prev => [...prev, aiMessage]);
updateSessionMessages([...messages, aiMessage]);
} catch (error) {
console.error("Error uploading file", error);
const errorMessage: ChatMessage = {
console.error('Error uploading file', error);
const errorMessage: Message = {
id: Date.now().toString(),
sender: 'ai',
text: "文件上传失败。",
timestamp: new Date()
role: 'assistant',
content: '文件上传失败。',
timestamp: Date.now(),
};
setMessages(prev => [...prev, errorMessage]);
updateSessionMessages([...messages, errorMessage]);
} finally {
setLoading(false);
if (fileInputRef.current) {
@@ -62,77 +76,116 @@ export function ChatPanel() {
};
const handleSendMessage = async () => {
if (!input.trim()) return;
if (!input.trim() || !activeSessionId) return;
const userMessage: ChatMessage = {
const userText = input;
const userMessage: Message = {
id: Date.now().toString(),
sender: 'user',
text: input,
timestamp: new Date()
role: 'user',
content: userText,
timestamp: Date.now(),
};
setMessages(prev => [...prev, userMessage]);
updateSessionMessages([...messages, userMessage]);
setInput('');
setLoading(true);
try {
// Assuming a token might be needed, apiClient should handle it if set
const promptModifier = mode === 'deploy' ? '[DEPLOY TASK] ' : '';
const response = await apiClient.post('/api/v1/adapter/client', {
message: promptModifier + userMessage.text
message: promptModifier + userMessage.content,
});
const aiMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
sender: 'ai',
text: typeof response.data.message === 'string' && response.data.message.includes('-')
? "Task has been created." // It's an event ID
: response.data.message || "I received your message.",
eventId: typeof response.data.message === 'string' && response.data.message.includes('-') ? response.data.message : undefined,
timestamp: new Date()
};
const responseData = response.data.message;
let aiContent = responseData || 'I received your message.';
setMessages(prev => [...prev, aiMessage]);
// If we got an event_id, we could potentially open a websocket to listen to its stream
if (aiMessage.eventId) {
console.log(`Open WS to track event: ${aiMessage.eventId}`);
// Implement WS tracking if needed
// Auto-update title if it's the first user message
if (messages.length <= 1 && userText.length > 0) {
setChatSessions((prev) =>
prev.map((s) =>
s.id === activeSessionId
? { ...s, title: userText.slice(0, 20) + (userText.length > 20 ? '...' : '') }
: s
)
);
}
} catch (error) {
console.error("Error sending message", error);
const errorMessage: ChatMessage = {
const aiMessage: Message = {
id: (Date.now() + 1).toString(),
sender: 'ai',
text: "Sorry, I encountered an error communicating with the server.",
timestamp: new Date()
role: 'assistant',
content: aiContent,
timestamp: Date.now(),
};
setMessages(prev => [...prev, errorMessage]);
updateSessionMessages([...messages, userMessage, aiMessage]);
} catch (error) {
console.error('Error sending message', error);
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: 'Sorry, I encountered an error communicating with the server.',
timestamp: Date.now(),
};
updateSessionMessages([...messages, userMessage, errorMessage]);
} finally {
setLoading(false);
}
};
const [mode, setMode] = useState<'chat' | 'deploy'>('chat');
if (!activeSessionId) {
return (
<div className="flex-1 flex flex-col bg-white overflow-hidden items-center justify-center">
<Activity size={48} className="text-slate-300 mb-4" />
<h2 className="text-xl font-semibold text-slate-600">Pretor Assistant</h2>
<p className="text-slate-400 mt-2">Select a chat history or create a new one to start.</p>
<button
onClick={() => {
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([newSession, ...chatSessions]);
setActiveSessionId(newSession.id);
}}
className="mt-6 px-6 py-2 bg-blue-200 text-slate-800 rounded-xl shadow-sm hover:bg-blue-300 transition-colors"
>
Start New Chat
</button>
</div>
);
}
// Notice we removed the outer padded div, since App.tsx is handling that layout now
return (
<div className="flex-1 flex flex-col bg-slate-50">
<div className="h-14 border-b border-slate-200 bg-white flex items-center justify-between px-6 shadow-sm z-10">
<div className="flex-1 flex flex-col bg-white overflow-hidden relative">
<div className="h-14 border-b border-slate-100 bg-white flex items-center justify-between px-6 z-10 shrink-0">
<div className="flex items-center">
<MessageSquare size={18} className="text-blue-600 mr-3" />
<h1 className="font-semibold text-slate-800">Pretor Assistant</h1>
<h1 className="font-semibold text-slate-800">{activeSession?.title || 'Chat'}</h1>
</div>
<div className="flex space-x-2 bg-slate-100 p-1 rounded-lg">
<div className="flex space-x-2 bg-slate-50 p-1 rounded-lg">
<button
onClick={() => setMode('chat')}
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${mode === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${
mode === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
}`}
>
Chat
</button>
<button
onClick={() => setMode('deploy')}
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${mode === 'deploy' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${
mode === 'deploy' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
}`}
>
Deploy Task
</button>
@@ -140,54 +193,52 @@ export function ChatPanel() {
</div>
{/* Chat History */}
<div className="flex-1 p-6 overflow-y-auto space-y-6">
<div className="flex justify-center">
<span className="text-xs text-slate-400 bg-slate-200/50 px-3 py-1 rounded-full">Today</span>
</div>
<div className="flex-1 p-6 overflow-y-auto space-y-6 bg-white">
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.sender === 'ai' && (
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.role === 'assistant' && (
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
<Activity size={16} className="text-blue-600" />
</div>
)}
<div className={`${msg.sender === 'user' ? 'bg-blue-600 text-white rounded-2xl rounded-tr-sm' : 'bg-white border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'} p-4 max-w-[80%] shadow-sm`}>
<p className="text-sm leading-relaxed mb-3">{msg.text}</p>
{msg.eventId && (
<div className="bg-slate-50 border border-slate-100 rounded-lg p-3 flex items-center text-sm">
<Terminal size={16} className="text-slate-400 mr-2" />
<span className="font-mono text-slate-600 text-xs">Task ID: {msg.eventId}</span>
</div>
<div
className={`${
msg.role === 'user'
? 'bg-blue-100 text-slate-800 rounded-2xl rounded-tr-sm'
: 'bg-slate-50 border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'
} p-4 max-w-[80%] shadow-sm`}
>
<p className="text-sm leading-relaxed mb-1 whitespace-pre-wrap">{msg.content}</p>
{typeof msg.content === 'string' && msg.content.includes('-') && msg.role === 'assistant' && (msg.content.length === 36 || msg.content.includes('任务已创建')) && (
<div className="mt-2 bg-white border border-slate-100 rounded-lg p-3 flex items-center text-sm shadow-sm">
<Terminal size={16} className="text-slate-400 mr-2" />
<span className="font-mono text-slate-600 text-xs">Task ID: {msg.content.substring(0, 36)}</span>
</div>
)}
</div>
</div>
))}
{loading && (
<div className="flex justify-start">
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
<Activity size={16} className="text-blue-600 animate-spin" />
</div>
<div className="bg-white border border-slate-100 text-slate-700 p-4 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
<span className="flex space-x-1">
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></span>
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-75"></span>
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
</span>
</div>
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
<Activity size={16} className="text-blue-600 animate-spin" />
</div>
<div className="bg-slate-50 border border-slate-100 text-slate-700 p-4 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
<span className="flex space-x-1">
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></span>
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-75"></span>
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Chat Input */}
<div className="p-4 bg-white border-t border-slate-200">
<div className="p-4 bg-white border-t border-slate-100 shrink-0">
<div className="relative flex items-center">
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
className="hidden"
/>
<input type="file" ref={fileInputRef} onChange={handleFileUpload} className="hidden" />
<button
onClick={() => fileInputRef.current?.click()}
className="absolute left-2 p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors z-10 cursor-pointer"
@@ -201,12 +252,12 @@ export function ChatPanel() {
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Ask Pretor to do something..."
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-xl pl-12 pr-12 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-inner"
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-2xl pl-12 pr-12 py-3.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all"
/>
<button
onClick={handleSendMessage}
disabled={loading || !input.trim()}
className="absolute right-2 p-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors shadow-sm disabled:opacity-50 cursor-pointer"
className="absolute right-2 p-1.5 bg-blue-200 text-slate-800 rounded-xl hover:bg-blue-300 transition-colors shadow-sm disabled:opacity-50 cursor-pointer"
>
<ChevronRight size={18} />
</button>
+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>
+117 -130
View File
@@ -1,35 +1,25 @@
import { useState, useEffect, useRef } from 'react';
import { Terminal, Activity, RefreshCw, CheckCircle2, Circle, XCircle, Clock, Loader2 } from 'lucide-react';
import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork } from 'lucide-react';
import apiClient from '../../api/client';
import type { WorkflowDetail, WorkflowStep } from '../../types';
import type { WorkflowDetail } from '../../types';
import { WorkflowDiagram } from './WorkflowDiagram';
interface RightPanelProps {
selectedWorkflow: string | null;
}
function stepStatusIcon(status: string) {
switch (status) {
case 'completed':
return <CheckCircle2 size={14} className="text-green-500" />;
case 'running':
return <Loader2 size={14} className="text-blue-500 animate-spin" />;
case 'failed':
return <XCircle size={14} className="text-red-500" />;
default:
return <Circle size={14} className="text-slate-300" />;
}
}
export function RightPanel({ selectedWorkflow }: RightPanelProps) {
const [detail, setDetail] = useState<WorkflowDetail | null>(null);
const [loading, setLoading] = useState(false);
const [logs, setLogs] = useState<string[]>([]);
const [sseConnected, setSseConnected] = useState(false);
const [replyText, setReplyText] = useState('');
const [activeTab, setActiveTab] = useState<'chat' | 'diagram'>('chat');
const eventSourceRef = useRef<EventSource | null>(null);
const logsEndRef = useRef<HTMLDivElement>(null);
const fetchDetail = async (traceId: string) => {
setLoading(true);
setLogs([]);
try {
const response = await apiClient.get(`/api/v1/workflow/${traceId}`);
setDetail(response.data);
@@ -48,6 +38,7 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
}
fetchDetail(selectedWorkflow);
setLogs([]); // Reset logs when changing workflow
const protocol = window.location.protocol;
const host = window.location.host;
@@ -55,17 +46,13 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
const es = new EventSource(`${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`);
eventSourceRef.current = es;
es.onopen = () => {
setSseConnected(true);
};
es.onopen = () => setSseConnected(true);
es.onmessage = (event) => {
setLogs(prev => [...prev, event.data]);
};
es.onerror = () => {
setSseConnected(false);
};
es.onerror = () => setSseConnected(false);
const interval = setInterval(() => {
fetchDetail(selectedWorkflow);
@@ -78,127 +65,127 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
};
}, [selectedWorkflow]);
const isActive = detail?.status === 'llm_working' || detail?.status === 'tool_working';
useEffect(() => {
if (activeTab === 'chat' && logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [logs, activeTab]);
if (!selectedWorkflow) {
return (
<div className="w-80 bg-white border-l border-slate-200 flex flex-col z-0 justify-center items-center p-6 text-center">
<Activity size={32} className="text-slate-300 mb-4" />
<h3 className="text-sm font-semibold text-slate-600">No Workflow Selected</h3>
<p className="text-xs text-slate-400 mt-2">Select a workflow from the left panel to view its details.</p>
</div>
);
}
const handleReplySubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!replyText.trim() || !selectedWorkflow) return;
const message = replyText.trim();
setReplyText('');
setLogs(prev => [...prev, `[You]: ${message}`]);
try {
await apiClient.post(`/api/v1/workflow/reply/${selectedWorkflow}`, { message });
} catch (err) {
console.error("Failed to send reply", err);
setLogs(prev => [...prev, `[System Error]: Failed to send reply.`]);
}
};
if (!selectedWorkflow) return null;
return (
<div className="w-80 bg-white border-l border-slate-200 flex flex-col z-0">
<div className="h-14 border-b border-slate-100 flex items-center px-4 justify-between bg-slate-50/50">
<h2 className="font-semibold text-slate-800 text-sm flex items-center">
<Terminal size={16} className="mr-2 text-slate-500" />
Workflow Detail
</h2>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs rounded-md font-medium border ${sseConnected ? 'bg-green-100 text-green-700 border-green-200' : 'bg-slate-100 text-slate-500 border-slate-200'}`}>
{sseConnected ? 'Live' : '--'}
<div className="flex-1 bg-white border-l border-slate-200 flex flex-col z-0 relative">
<div className="h-14 border-b border-slate-100 flex items-center px-6 justify-between bg-white z-10 shrink-0">
<div className="flex items-center gap-4">
<h2 className="font-semibold text-slate-800 flex items-center gap-2">
<Terminal size={18} className="text-blue-500" />
<span className="truncate max-w-[200px]" title={detail?.workflow_title || 'Loading...'}>
{detail?.workflow_title || 'Workflow Details'}
</span>
</h2>
<span className={`px-2 py-0.5 text-xs rounded-full font-medium ${sseConnected ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>
{sseConnected ? 'Live' : 'Disconnected'}
</span>
<button
onClick={() => selectedWorkflow && fetchDetail(selectedWorkflow)}
className="p-1 text-slate-400 hover:text-blue-600 rounded transition-colors"
title="Refresh"
>
<RefreshCw size={14} />
</button>
</div>
{/* Navigation Tabs */}
<div className="flex items-center bg-slate-100 rounded-lg p-1">
<button
onClick={() => setActiveTab('chat')}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${activeTab === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
>
<LayoutList size={16} />
</button>
<button
onClick={() => setActiveTab('diagram')}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${activeTab === 'diagram' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
>
<GitFork size={16} />
</button>
</div>
<button
onClick={() => fetchDetail(selectedWorkflow)}
className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Refresh Data"
>
<RefreshCw size={16} className={loading ? "animate-spin" : ""} />
</button>
</div>
<div className="flex-1 p-4 overflow-y-auto">
{loading && !detail ? (
<div className="text-center text-slate-400 text-sm py-8">
<Loader2 size={24} className="animate-spin mx-auto mb-2" />
Loading...
<div className="flex-1 flex overflow-hidden bg-slate-50 relative">
{activeTab === 'diagram' ? (
<div className="absolute inset-0">
{detail?.steps && detail.steps.length > 0 ? (
<WorkflowDiagram steps={detail.steps} currentStep={detail.current_step} status={detail.status} />
) : (
<div className="h-full flex items-center justify-center text-slate-400">
Workflow steps are not yet generated.
</div>
)}
</div>
) : !detail ? (
<div className="text-center text-slate-400 text-sm py-8">Failed to load workflow details</div>
) : (
<>
{/* Header */}
<div className="mb-4">
<h3 className="text-base font-bold text-slate-800">
{detail.workflow_title || 'Workflow'}
</h3>
<p className="text-xs text-slate-500 font-mono mt-1">ID: {detail.event_id}</p>
{detail.command && (
<p className="text-xs text-slate-500 mt-1 truncate">Command: {detail.command}</p>
)}
<div className="flex items-center gap-2 mt-2">
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
detail.status === 'failed' ? 'bg-red-100 text-red-700' :
isActive ? 'bg-blue-100 text-blue-700' :
detail.status === 'waiting_llm_working' || detail.status === 'waiting_tool_working' ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
{detail.status}
</span>
<span className="text-xs text-slate-400">
Step {detail.current_step}/{detail.steps.length}
</span>
</div>
</div>
{/* Steps */}
{detail.steps.length > 0 && (
<div className="mb-4">
<h4 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Steps</h4>
<div className="space-y-1.5">
{detail.steps.map((step: WorkflowStep) => (
<div
key={step.step}
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs border ${
step.step === detail.current_step && isActive
? 'border-blue-200 bg-blue-50'
: step.status === 'completed'
? 'border-green-100 bg-green-50/50'
: step.status === 'failed'
? 'border-red-100 bg-red-50/50'
: 'border-slate-100 bg-white'
}`}
>
{stepStatusIcon(step.status)}
<span className="font-medium text-slate-700 w-5 text-right">{step.step}</span>
<span className="text-slate-600 truncate flex-1">{step.name}</span>
<span className="text-slate-400 text-[10px]">{step.node}</span>
</div>
))}
<div className="flex-1 flex flex-col p-6 overflow-hidden">
{/* Command Header */}
{detail?.command && (
<div className="bg-white border border-slate-200 rounded-xl p-4 mb-4 shadow-sm shrink-0">
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Original Command</h3>
<p className="text-slate-700 text-sm">{detail.command}</p>
</div>
</div>
)}
)}
{detail.steps.length === 0 && (
<div className="text-center py-4">
<Clock size={24} className="text-slate-300 mx-auto mb-2" />
<p className="text-xs text-slate-400">Workflow is being generated...</p>
</div>
)}
{/* SSE Logs */}
{logs.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Live Logs</h4>
<div className="relative border-l-2 border-slate-200 ml-3 pl-5 space-y-3">
{logs.map((msg, idx) => (
<div key={idx} className="relative">
<div className={`absolute -left-[27px] top-1 w-3 h-3 rounded-full border-2 border-white shadow-sm ${idx === logs.length - 1 && sseConnected ? 'bg-blue-500 animate-pulse' : 'bg-green-500'}`} />
<p className="text-[11px] font-mono text-slate-600 leading-relaxed break-all">{msg}</p>
{/* Live Chat / Logs Area */}
<div className="flex-1 bg-white border border-slate-200 rounded-xl shadow-sm overflow-y-auto p-4 mb-4 space-y-3 font-mono text-sm">
{logs.length === 0 ? (
<div className="h-full flex items-center justify-center text-slate-400">
Waiting for events...
</div>
) : (
logs.map((log, index) => (
<div key={index} className={`p-3 rounded-lg ${log.startsWith('[You]') ? 'bg-blue-50 border border-blue-100 text-blue-800 self-end ml-12' : 'bg-slate-50 border border-slate-100 text-slate-700 mr-12'}`}>
{log}
</div>
))}
</div>
</div>
)}
))
)}
<div ref={logsEndRef} />
</div>
{logs.length === 0 && sseConnected && isActive && (
<div className="text-xs text-slate-400 italic mt-2">Waiting for live events...</div>
)}
</>
{/* Input Area */}
<form onSubmit={handleReplySubmit} className="relative shrink-0">
<input
type="text"
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
placeholder="Reply to the workflow..."
className="w-full bg-white border border-slate-200 rounded-xl pl-4 pr-12 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm"
/>
<button
type="submit"
disabled={!replyText.trim()}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:hover:bg-blue-600 transition-colors"
>
<SendHorizontal size={16} />
</button>
</form>
</div>
)}
</div>
</div>
@@ -0,0 +1,118 @@
import { useEffect, useMemo } from 'react';
import {
ReactFlow,
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
MarkerType,
BackgroundVariant
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import type { WorkflowStep } from '../../types';
interface WorkflowDiagramProps {
steps: WorkflowStep[];
currentStep: number;
status: string;
}
export function WorkflowDiagram({ steps, currentStep, status }: WorkflowDiagramProps) {
const isWorkflowActive = status === 'llm_working' || status === 'tool_working';
const initialNodes = useMemo(() => {
return steps.map((step, index) => {
const isCurrent = step.step === currentStep && isWorkflowActive;
const isCompleted = step.status === 'completed';
const isFailed = step.status === 'failed';
let bgColor = '#ffffff';
let borderColor = '#e2e8f0'; // slate-200
let textColor = '#334155'; // slate-700
if (isCurrent) {
bgColor = '#eff6ff'; // blue-50
borderColor = '#3b82f6'; // blue-500
textColor = '#1e40af'; // blue-800
} else if (isFailed) {
bgColor = '#fef2f2'; // red-50
borderColor = '#ef4444'; // red-500
textColor = '#991b1b'; // red-800
} else if (isCompleted) {
bgColor = '#f0fdf4'; // green-50
borderColor = '#22c55e'; // green-500
textColor = '#166534'; // green-800
}
return {
id: step.step.toString(),
position: { x: 250, y: index * 120 + 50 },
data: {
label: (
<div className="flex flex-col items-center p-2 min-w-[150px]">
<div className="text-xs font-semibold mb-1 opacity-70 uppercase tracking-wider">{step.node}</div>
<div className="text-sm font-medium">{step.name}</div>
</div>
)
},
style: {
background: bgColor,
border: `2px solid ${borderColor}`,
borderRadius: '8px',
color: textColor,
boxShadow: isCurrent ? '0 4px 6px -1px rgba(59, 130, 246, 0.2)' : '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
padding: '4px',
}
};
});
}, [steps, currentStep, isWorkflowActive]);
const initialEdges = useMemo(() => {
const edges = [];
for (let i = 0; i < steps.length - 1; i++) {
edges.push({
id: `e${steps[i].step}-${steps[i + 1].step}`,
source: steps[i].step.toString(),
target: steps[i + 1].step.toString(),
animated: steps[i].step === currentStep && isWorkflowActive,
style: { stroke: '#cbd5e1', strokeWidth: 2 },
markerEnd: {
type: MarkerType.ArrowClosed,
color: '#cbd5e1',
},
});
}
return edges;
}, [steps, currentStep, isWorkflowActive]);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
// Update nodes and edges if props change
useEffect(() => {
setNodes(initialNodes);
setEdges(initialEdges);
}, [initialNodes, initialEdges, setNodes, setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
fitView
attributionPosition="bottom-right"
>
<Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#cbd5e1" />
<Controls className="bg-white border-slate-200 fill-slate-500 shadow-sm rounded-md" />
<MiniMap
nodeColor={(n) => {
if (n.style?.background) return n.style.background as string;
return '#e2e8f0';
}}
maskColor="rgba(248, 250, 252, 0.7)"
/>
</ReactFlow>
);
}
@@ -0,0 +1,128 @@
import { useState, useEffect } from 'react';
import apiClient from '../../api/client';
import type { Workflow } from '../../types';
import { PlayCircle, CheckCircle, XCircle, Clock } from 'lucide-react';
interface WorkflowListViewProps {
onSelectWorkflow: (id: string) => void;
}
export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
const [workflows, setWorkflows] = useState<Workflow[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchWorkflows = async () => {
try {
const response = await apiClient.get('/api/v1/workflow/list');
const data = response.data;
let parsedWorkflows: Workflow[] = [];
if (Array.isArray(data)) {
parsedWorkflows = data;
} else if (data && typeof data === 'object') {
parsedWorkflows = data.workflows || Object.values(data);
}
setWorkflows(parsedWorkflows);
} catch (error) {
console.error('Failed to fetch workflows', error);
} finally {
setLoading(false);
}
};
fetchWorkflows();
const intervalId = setInterval(fetchWorkflows, 5000);
return () => clearInterval(intervalId);
}, []);
const getStatusIcon = (status?: string) => {
if (status === 'completed') return <CheckCircle size={20} className="text-green-500" />;
if (status === 'failed') return <XCircle size={20} className="text-red-500" />;
if (status === 'llm_working' || status === 'tool_working' || status?.includes('working'))
return <PlayCircle size={20} className="text-blue-500 animate-pulse" />;
return <Clock size={20} className="text-slate-400" />;
};
const getStatusBadge = (status?: string) => {
let colorClass = "bg-slate-100 text-slate-600";
let label = "Waiting";
if (status === 'completed') { colorClass = "bg-green-100 text-green-700"; label = "Completed"; }
else if (status === 'failed') { colorClass = "bg-red-100 text-red-700"; label = "Failed"; }
else if (status?.includes('working')) { colorClass = "bg-blue-100 text-blue-700 animate-pulse"; label = "Running"; }
return (
<span className={`px-2.5 py-1 text-xs font-medium rounded-full ${colorClass}`}>
{label}
</span>
);
};
if (loading) {
return (
<div className="flex-1 flex items-center justify-center p-6 bg-slate-50">
<div className="text-slate-400">Loading Workflows...</div>
</div>
);
}
return (
<div className="flex-1 flex flex-col p-8 bg-slate-50 overflow-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-800">Workflows</h1>
<p className="text-slate-500 mt-1">Manage and monitor your automated processes.</p>
</div>
{workflows.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center border-2 border-dashed border-slate-200 rounded-2xl bg-white p-12 text-center">
<div className="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4">
<PlayCircle size={32} className="text-slate-400" />
</div>
<h3 className="text-lg font-medium text-slate-800 mb-2">No Workflows Found</h3>
<p className="text-slate-500 max-w-sm">
Workflows created from your chats will appear here automatically.
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{workflows.map((wf) => (
<div
key={wf.event_id}
onClick={() => onSelectWorkflow(wf.event_id)}
className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm hover:shadow-md hover:border-blue-300 transition-all cursor-pointer group flex flex-col h-full"
>
<div className="flex justify-between items-start mb-4">
<div className="p-2.5 bg-blue-50 text-blue-600 rounded-xl group-hover:bg-blue-600 group-hover:text-white transition-colors">
{getStatusIcon(wf.status)}
</div>
{getStatusBadge(wf.status)}
</div>
<h3 className="text-lg font-semibold text-slate-800 mb-2 line-clamp-1" title={wf.workflow_title || 'Unnamed Workflow'}>
{wf.workflow_title || 'Unnamed Workflow'}
</h3>
<div className="mt-auto">
{wf.message && (
<div className="text-sm text-slate-500 line-clamp-2 mt-4 bg-slate-50 p-3 rounded-lg border border-slate-100">
<span className="font-medium text-slate-600 block mb-1">Command:</span>
"{wf.message}"
</div>
)}
<div className="flex justify-between items-center mt-5 text-xs text-slate-400">
<span className="font-mono bg-slate-100 px-2 py-1 rounded truncate max-w-[140px]" title={wf.event_id}>
{wf.event_id}
</span>
{wf.create_time && (
<span>{new Date(wf.create_time).toLocaleDateString()}</span>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}
@@ -0,0 +1,100 @@
import { ChevronLeft, ChevronRight, MessageSquare, Workflow, Box, Bot } from 'lucide-react';
interface CollapsibleSidebarProps {
mode: 'work' | 'agent';
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
workTab: 'chat' | 'workflow';
setWorkTab: (tab: 'chat' | 'workflow') => void;
agentTab: 'plugin' | 'agents';
setAgentTab: (tab: 'plugin' | 'agents') => void;
}
export function CollapsibleSidebar({
mode,
isOpen,
setIsOpen,
workTab,
setWorkTab,
agentTab,
setAgentTab,
}: CollapsibleSidebarProps) {
const getWorkNav = () => (
<>
<button
onClick={() => setWorkTab('chat')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
workTab === 'chat'
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
}`}
>
<MessageSquare size={18} className="mr-3" />
{isOpen && <span>Chat</span>}
</button>
<button
onClick={() => setWorkTab('workflow')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
workTab === 'workflow'
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
}`}
>
<Workflow size={18} className="mr-3" />
{isOpen && <span>Workflow</span>}
</button>
</>
);
const getAgentNav = () => (
<>
<button
onClick={() => setAgentTab('plugin')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
agentTab === 'plugin'
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
}`}
>
<Box size={18} className="mr-3" />
{isOpen && <span>Plugin</span>}
</button>
<button
onClick={() => setAgentTab('agents')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
agentTab === 'agents'
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
}`}
>
<Bot size={18} className="mr-3" />
{isOpen && <span>Agents</span>}
</button>
</>
);
// Background is slightly darker than the page background but lighter than the topbar
// Let's make it the "main nav light blue" level. We will use bg-blue-50 for it,
// and make the page background essentially white or extremely light slate,
// but since the instruction says "page background is the next lightest",
// let's use bg-blue-100 for Sidebar and bg-blue-50 for page background.
return (
<div
className={`bg-blue-50 border-r border-blue-100 flex flex-col transition-all duration-300 relative z-10 ${
isOpen ? 'w-64' : 'w-16'
}`}
>
<div className="flex-1 py-4 space-y-2 overflow-y-auto">
{mode === 'work' ? getWorkNav() : getAgentNav()}
</div>
<button
onClick={() => setIsOpen(!isOpen)}
className="h-10 w-10 absolute -right-5 top-1/2 -translate-y-1/2 bg-white border border-blue-200 rounded-full flex items-center justify-center text-slate-400 hover:text-blue-600 hover:border-blue-400 shadow-sm transition-all z-20"
>
{isOpen ? <ChevronLeft size={16} /> : <ChevronRight size={16} />}
</button>
</div>
);
}
@@ -1,51 +0,0 @@
import { Activity, MessageSquare, Settings, Bot, Box } from 'lucide-react';
interface SidebarProps {
currentView: string;
setCurrentView: (view: string) => void;
}
export function Sidebar({ currentView, setCurrentView }: SidebarProps) {
return (
<div className="w-12 bg-white border-r border-slate-200 flex flex-col items-center py-4 space-y-6 shadow-sm z-10 shrink-0">
<div
className="w-8 h-8 bg-blue-600 rounded-xl flex items-center justify-center text-white shadow-md shadow-blue-200 cursor-pointer hover:bg-blue-700 transition-colors"
onClick={() => setCurrentView('dashboard')}
>
<Activity size={18} />
</div>
<div className="flex flex-col space-y-4 flex-1 mt-8">
<button
onClick={() => setCurrentView('dashboard')}
className={`p-1.5 rounded-lg transition-colors ${currentView === 'dashboard' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
title="Chat"
>
<MessageSquare size={18} />
</button>
<button
onClick={() => setCurrentView('agent')}
className={`p-1.5 rounded-lg transition-colors ${currentView === 'agent' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
title="Agents"
>
<Bot size={18} />
</button>
<button
onClick={() => setCurrentView('resource')}
className={`p-1.5 rounded-lg transition-colors ${currentView === 'resource' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
title="Resources"
>
<Box size={18} />
</button>
<button
onClick={() => setCurrentView('settings')}
className={`p-1.5 rounded-lg transition-colors ${currentView === 'settings' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
title="Settings"
>
<Settings size={18} />
</button>
</div>
</div>
);
}
+59
View File
@@ -0,0 +1,59 @@
import { Settings, BrainCircuit } from 'lucide-react';
interface TopBarProps {
mode: 'work' | 'agent';
setMode: (mode: 'work' | 'agent') => void;
showSettings: boolean;
setShowSettings: (show: boolean) => void;
}
export function TopBar({ mode, setMode, showSettings, setShowSettings }: TopBarProps) {
return (
<div className="h-14 bg-blue-100 text-blue-900 flex items-center justify-between px-4 shrink-0 shadow-sm z-20 relative">
{/* Left: Logo */}
<div className="flex items-center space-x-2 font-bold text-xl tracking-tight text-blue-900">
<BrainCircuit className="text-blue-600" size={24} />
<span>Pretor</span>
</div>
{/* Right Container: Mode Toggle Switch + Settings */}
<div className="flex items-center space-x-4">
{/* Mode Toggle Switch */}
<div className="flex items-center space-x-1 bg-blue-200/50 p-1 rounded-full border border-blue-200">
<button
onClick={() => { setMode('work'); setShowSettings(false); }}
className={`px-4 py-1.5 rounded-full text-sm font-medium transition-all ${
mode === 'work' && !showSettings
? 'bg-white text-blue-700 shadow-sm'
: 'text-blue-700 hover:text-blue-900 hover:bg-white/50'
}`}
>
Work
</button>
<button
onClick={() => { setMode('agent'); setShowSettings(false); }}
className={`px-4 py-1.5 rounded-full text-sm font-medium transition-all ${
mode === 'agent' && !showSettings
? 'bg-white text-blue-700 shadow-sm'
: 'text-blue-700 hover:text-blue-900 hover:bg-white/50'
}`}
>
Agent
</button>
</div>
{/* Settings */}
<button
onClick={() => setShowSettings(!showSettings)}
className={`p-2 rounded-full transition-colors ${
showSettings ? 'bg-white text-blue-700 shadow-sm' : 'text-blue-700 hover:bg-white/50 hover:text-blue-900'
}`}
title="Settings"
>
<Settings size={20} />
</button>
</div>
</div>
);
}
@@ -0,0 +1,49 @@
import { SkillSettings } from './SkillSettings';
import { ToolSettings } from './ToolSettings';
import { WorkflowTemplateSettings } from './WorkflowTemplateSettings';
interface PluginLayoutProps {
resourceTab: string;
setResourceTab: (tab: string) => void;
}
export function PluginLayout({ resourceTab, setResourceTab }: PluginLayoutProps) {
return (
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
{/* Top Tabs for Plugin Module */}
<div className="h-14 border-b border-slate-200 bg-white flex items-center px-6 shadow-sm z-10 shrink-0 space-x-6">
<button
onClick={() => setResourceTab('skill')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
resourceTab === 'skill' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
}`}
>
Skills
</button>
<button
onClick={() => setResourceTab('workflow_template')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
resourceTab === 'workflow_template' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
}`}
>
Workflow Templates
</button>
<button
onClick={() => setResourceTab('tool')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
resourceTab === 'tool' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
}`}
>
Tools
</button>
</div>
{/* Main Content */}
<div className="flex-1 overflow-y-auto p-8">
{resourceTab === 'skill' && <SkillSettings />}
{resourceTab === 'workflow_template' && <WorkflowTemplateSettings />}
{resourceTab === 'tool' && <ToolSettings />}
</div>
</div>
);
}
@@ -79,7 +79,7 @@ export function SkillSettings() {
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100 flex items-center space-x-3">
<div className="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-lg flex items-center justify-center">
<div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-lg flex items-center justify-center">
<Download size={20} />
</div>
<div>
@@ -98,7 +98,7 @@ export function SkillSettings() {
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/user/repo"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
@@ -108,7 +108,7 @@ export function SkillSettings() {
value={path}
onChange={(e) => setPath(e.target.value)}
placeholder="e.g. subfolder/path"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
@@ -120,7 +120,7 @@ export function SkillSettings() {
<button
type="submit"
disabled={installing}
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<Plus size={16} className="mr-2" />
{installing ? 'Installing...' : 'Install'}
@@ -88,7 +88,7 @@ export function WorkflowTemplateSettings() {
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100 flex items-center space-x-3">
<div className="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-lg flex items-center justify-center">
<div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-lg flex items-center justify-center">
<FileCode size={20} />
</div>
<div>
@@ -105,7 +105,7 @@ export function WorkflowTemplateSettings() {
rows={8}
value={templateJson}
onChange={(e) => setTemplateJson(e.target.value)}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
/>
</div>
@@ -116,7 +116,7 @@ export function WorkflowTemplateSettings() {
<button
type="submit"
disabled={creating}
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<Plus size={16} className="mr-2" />
{creating ? 'Creating...' : 'Create Template'}
@@ -1,52 +0,0 @@
import { Wrench, Database, FileCode } from 'lucide-react';
import { SkillSettings } from './SkillSettings';
import { ToolSettings } from './ToolSettings';
import { WorkflowTemplateSettings } from './WorkflowTemplateSettings';
interface ResourceLayoutProps {
resourceTab: string;
setResourceTab: (tab: string) => void;
}
export function ResourceLayout({ resourceTab, setResourceTab }: ResourceLayoutProps) {
return (
<div className="flex-1 flex bg-slate-50 overflow-hidden">
{/* Resource Inner Sidebar */}
<div className="w-64 bg-white border-r border-slate-200 flex flex-col z-0">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-800">Resources</h2>
</div>
<div className="flex-1 p-4 space-y-2 overflow-y-auto">
<button
onClick={() => setResourceTab('skill')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'skill' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<Wrench size={18} className="mr-3" />
Skills
</button>
<button
onClick={() => setResourceTab('workflow_template')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'workflow_template' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<FileCode size={18} className="mr-3" />
Workflow Templates
</button>
<button
onClick={() => setResourceTab('tool')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'tool' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<Database size={18} className="mr-3" />
Tools
</button>
</div>
</div>
{/* Resource Main Content */}
<div className="flex-1 overflow-y-auto p-8">
{resourceTab === 'skill' && <SkillSettings />}
{resourceTab === 'workflow_template' && <WorkflowTemplateSettings />}
{resourceTab === 'tool' && <ToolSettings />}
</div>
</div>
);
}
+2
View File
@@ -65,6 +65,8 @@ export interface Workflow {
event_id: string;
workflow_title: string;
status?: string;
message?: string;
create_time?: string;
}
export interface WorkflowStep {