✨ [Feature] Add frontend authentication page and 401 error interceptor (#21)
* feat: add frontend authentication page and 401 interceptor Adds a new AuthPage component for user login and registration, integrates it into App.tsx to protect routes, and sets up an Axios interceptor to handle 401 Unauthorized responses by clearing local storage and reloading. Also fixes a missing logger attribute in WorkflowEngine for backend tests. Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com> * fix: gracefully handle closed websockets Updates the websocket endpoints in `pretor/api/cluster.py` and `pretor/api/workflow.py` to catch `RuntimeError` alongside `WebSocketDisconnect`. This prevents the application from crashing and spamming error logs when the frontend client unexpectedly closes the connection and the underlying TCP transport is closed. Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com> * feat: add worker form and update global settings Adds a new form in the Worker Individual Settings page to create custom worker individuals via the `/api/v1/agent/worker` endpoint. Also updates the System Settings page to remove the obsolete "Max Concurrent Workflows" setting and makes the system language and theme toggles functional by persisting to local storage and updating the document root class. Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>
This commit is contained in:
parent
dcf53524b2
commit
3a96f287c7
|
|
@ -1,18 +1,36 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Sidebar } from './components/Layout/Sidebar';
|
||||
import { MonitoringLayout } from './components/Monitoring/MonitoringLayout';
|
||||
import { SettingsLayout } from './components/Settings/SettingsLayout';
|
||||
import { AgentLayout } from './components/Agent/AgentLayout';
|
||||
import { ResourceLayout } from './components/Resource/ResourceLayout';
|
||||
import { LeftPanel } from './components/Chat/LeftPanel';
|
||||
import { ChatPanel } from './components/Chat/ChatPanel';
|
||||
import { RightPanel } from './components/Chat/RightPanel';
|
||||
import { AuthPage } from './components/Auth/AuthPage';
|
||||
|
||||
function App() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('chats'); // For LeftPanel
|
||||
const [currentView, setCurrentView] = useState('dashboard'); // 'dashboard', 'settings', or 'monitoring'
|
||||
const [currentView, setCurrentView] = useState('dashboard'); // 'dashboard', 'settings', 'monitoring', 'agent', 'resource'
|
||||
const [settingsTab, setSettingsTab] = useState('users'); // For SettingsLayout
|
||||
const [agentTab, setAgentTab] = useState('worker'); // For AgentLayout
|
||||
const [resourceTab, setResourceTab] = useState('skill'); // For ResourceLayout
|
||||
|
||||
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if token exists in localStorage on mount
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <AuthPage onLoginSuccess={() => setIsAuthenticated(true)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
|
||||
|
||||
|
|
@ -22,6 +40,10 @@ function App() {
|
|||
{/* Main Content Area depending on view */}
|
||||
{currentView === 'monitoring' ? (
|
||||
<MonitoringLayout />
|
||||
) : currentView === 'agent' ? (
|
||||
<AgentLayout agentTab={agentTab} setAgentTab={setAgentTab} />
|
||||
) : currentView === 'resource' ? (
|
||||
<ResourceLayout resourceTab={resourceTab} setResourceTab={setResourceTab} />
|
||||
) : currentView === 'dashboard' ? (
|
||||
<>
|
||||
{/* 2. Left Panel - Cluster Status & Workflows/Chats */}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,18 @@ apiClient.interceptors.request.use((config) => {
|
|||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to catch 401 Unauthorized errors and force login
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
// Clear token
|
||||
localStorage.removeItem('token');
|
||||
// Reload the page to force the user back to the Auth view
|
||||
window.location.reload();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { Bot, Key } from 'lucide-react';
|
||||
import { ProvidersSettings } from './ProvidersSettings';
|
||||
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
|
||||
|
||||
interface AgentLayoutProps {
|
||||
agentTab: string;
|
||||
setAgentTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
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" />
|
||||
Worker 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>
|
||||
|
||||
{/* Agent Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{agentTab === 'worker' && <WorkerIndividualSettings />}
|
||||
{agentTab === 'providers' && <ProvidersSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
import { useState } from 'react';
|
||||
import apiClient from '../../api/client';
|
||||
import { Bot, Save } from 'lucide-react';
|
||||
|
||||
function WorkerIndividualForm() {
|
||||
const [formData, setFormData] = useState({
|
||||
agent_name: '',
|
||||
agent_type: 'OrdinaryIndividual',
|
||||
description: '',
|
||||
provider_title: '',
|
||||
model_id: '',
|
||||
system_prompt: '',
|
||||
output_template: '{}',
|
||||
bound_skill: '{}',
|
||||
workspace: '[]'
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setMessage('');
|
||||
try {
|
||||
const payload = {
|
||||
...formData,
|
||||
output_template: JSON.parse(formData.output_template),
|
||||
bound_skill: JSON.parse(formData.bound_skill),
|
||||
workspace: JSON.parse(formData.workspace)
|
||||
};
|
||||
await apiClient.post('/api/v1/agent/worker', payload);
|
||||
setMessage('Successfully created worker individual');
|
||||
setFormData({
|
||||
agent_name: '',
|
||||
agent_type: 'OrdinaryIndividual',
|
||||
description: '',
|
||||
provider_title: '',
|
||||
model_id: '',
|
||||
system_prompt: '',
|
||||
output_template: '{}',
|
||||
bound_skill: '{}',
|
||||
workspace: '[]'
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setMessage(err.response?.data?.detail || 'Failed to create worker individual. Ensure JSON fields are valid.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Name</label>
|
||||
<input required type="text" name="agent_name" value={formData.agent_name} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Type</label>
|
||||
<select name="agent_type" value={formData.agent_type} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
<option value="OrdinaryIndividual">OrdinaryIndividual</option>
|
||||
<option value="SkillIndividual">SkillIndividual</option>
|
||||
<option value="SpecialIndividual">SpecialIndividual</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Description</label>
|
||||
<input required type="text" name="description" value={formData.description} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
|
||||
<input required type="text" name="provider_title" value={formData.provider_title} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Model ID</label>
|
||||
<input required type="text" name="model_id" value={formData.model_id} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">System Prompt</label>
|
||||
<textarea required name="system_prompt" value={formData.system_prompt} onChange={handleChange} rows={3} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Output Template (JSON dict)</label>
|
||||
<input required type="text" name="output_template" value={formData.output_template} onChange={handleChange} 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Bound Skill (JSON dict)</label>
|
||||
<input required type="text" name="bound_skill" value={formData.bound_skill} onChange={handleChange} 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Workspace (JSON list)</label>
|
||||
<input required type="text" name="workspace" value={formData.workspace} onChange={handleChange} 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" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`p-3 rounded-lg text-sm ${message.includes('Success') ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-2">
|
||||
<button type="submit" disabled={loading} className="flex items-center space-x-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50">
|
||||
<Save size={16} />
|
||||
<span>{loading ? 'Creating...' : 'Create Worker'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkerIndividualSettings() {
|
||||
const [nodeType, setNodeType] = useState('supervisory_node');
|
||||
const [providerTitle, setProviderTitle] = useState('');
|
||||
const [modelId, setModelId] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const handleCreateNode = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setMessage('');
|
||||
try {
|
||||
await apiClient.post('/api/v1/agent', {
|
||||
provider_title: providerTitle,
|
||||
model_id: modelId,
|
||||
individual_name: nodeType
|
||||
});
|
||||
setMessage(`Successfully loaded ${nodeType}`);
|
||||
setProviderTitle('');
|
||||
setModelId('');
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setMessage(err.response?.data?.detail || 'Failed to load agent node');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-800">Worker Individual Settings</h1>
|
||||
<p className="text-slate-500 mt-1">Configure your system agents and custom workers.</p>
|
||||
</div>
|
||||
|
||||
<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 justify-between">
|
||||
<div className="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">
|
||||
<Bot size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-800">System Nodes</h2>
|
||||
<p className="text-sm text-slate-500">Initialize core system agents</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleCreateNode} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Node Type</label>
|
||||
<select
|
||||
value={nodeType}
|
||||
onChange={(e) => setNodeType(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"
|
||||
>
|
||||
<option value="supervisory_node">Supervisory Node</option>
|
||||
<option value="consciousness_node">Consciousness Node</option>
|
||||
<option value="control_node">Control Node</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={providerTitle}
|
||||
onChange={(e) => setProviderTitle(e.target.value)}
|
||||
placeholder="e.g. openai"
|
||||
required
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Model ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={modelId}
|
||||
onChange={(e) => setModelId(e.target.value)}
|
||||
placeholder="e.g. gpt-4"
|
||||
required
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`p-3 rounded-lg text-sm ${message.includes('Success') ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Save size={16} />
|
||||
<span>{loading ? 'Saving...' : 'Load Node'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100">
|
||||
<h2 className="text-lg font-semibold text-slate-800">Create Worker Individual</h2>
|
||||
<p className="text-sm text-slate-500">Add a new custom worker to the system.</p>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<WorkerIndividualForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import React, { useState } from 'react';
|
||||
import apiClient from '../../api/client';
|
||||
import { Activity } from 'lucide-react';
|
||||
|
||||
interface AuthPageProps {
|
||||
onLoginSuccess: () => void;
|
||||
}
|
||||
|
||||
export function AuthPage({ onLoginSuccess }: AuthPageProps) {
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [userName, setUserName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
if (isLogin) {
|
||||
// Login
|
||||
const response = await apiClient.post('/api/v1/auth/login', {
|
||||
user_name: userName,
|
||||
password: password,
|
||||
});
|
||||
|
||||
if (response.data.token) {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
onLoginSuccess();
|
||||
}
|
||||
} else {
|
||||
// Register
|
||||
const response = await apiClient.post('/api/v1/auth/register', {
|
||||
user_name: userName,
|
||||
password: password,
|
||||
});
|
||||
|
||||
// After successful register, we can automatically log them in
|
||||
// or just show a message and switch to login. Let's switch to login.
|
||||
if (response.data.message === 'success') {
|
||||
setIsLogin(true);
|
||||
setError('Registration successful. Please log in.');
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setError(err.response?.data?.detail || err.response?.data?.message || 'Authentication failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center bg-slate-50">
|
||||
<div className="w-full max-w-md bg-white rounded-xl shadow-lg p-8 border border-slate-100">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center text-white mb-4 shadow-md shadow-blue-200">
|
||||
<Activity size={24} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800">
|
||||
{isLogin ? 'Welcome Back' : 'Create Account'}
|
||||
</h2>
|
||||
<p className="text-slate-500 mt-2 text-sm">
|
||||
{isLogin ? 'Enter your credentials to access your account' : 'Sign up to start using the platform'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className={`mb-4 p-3 rounded-lg text-sm ${error.includes('successful') ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'}`}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={userName}
|
||||
onChange={(e) => setUserName(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-2.5 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Processing...' : (isLogin ? 'Sign In' : 'Sign Up')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-slate-500">
|
||||
{isLogin ? "Don't have an account? " : "Already have an account? "}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLogin(!isLogin);
|
||||
setError('');
|
||||
}}
|
||||
className="text-blue-600 font-medium hover:text-blue-700 focus:outline-none"
|
||||
>
|
||||
{isLogin ? 'Sign up' : 'Sign in'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { Activity, MessageSquare, MonitorPlay, Settings } from 'lucide-react';
|
||||
import { Activity, MessageSquare, MonitorPlay, Settings, Bot, Box } from 'lucide-react';
|
||||
|
||||
interface SidebarProps {
|
||||
currentView: string;
|
||||
|
|
@ -31,6 +31,20 @@ export function Sidebar({ currentView, setCurrentView }: SidebarProps) {
|
|||
>
|
||||
<MonitorPlay 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'}`}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { Wrench, Database } from 'lucide-react';
|
||||
import { SkillSettings } from './SkillSettings';
|
||||
import { ResourceSettings } from './ResourceSettings';
|
||||
|
||||
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('resource')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'resource' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||
>
|
||||
<Database size={18} className="mr-3" />
|
||||
Resources
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resource Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{resourceTab === 'skill' && <SkillSettings />}
|
||||
{resourceTab === 'resource' && <ResourceSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
export function ResourceSettings() {
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-800">Resource Management</h1>
|
||||
<p className="text-slate-500 mt-1">Manage external and internal resources.</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden p-6 text-slate-500 text-sm">
|
||||
Resource management configuration coming soon...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
export function SkillSettings() {
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-800">Skill Management</h1>
|
||||
<p className="text-slate-500 mt-1">Manage agent skills and functions.</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden p-6 text-slate-500 text-sm">
|
||||
Skill management configuration coming soon...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import { Users, Key, Sliders } from 'lucide-react';
|
||||
import { Users, Sliders } from 'lucide-react';
|
||||
import { UsersSettings } from './UsersSettings';
|
||||
import { ProvidersSettings } from './ProvidersSettings';
|
||||
import { SystemSettings } from './SystemSettings';
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
|
|
@ -25,13 +24,6 @@ export function SettingsLayout({ settingsTab, setSettingsTab }: SettingsLayoutPr
|
|||
<Users size={18} className="mr-3" />
|
||||
User Management
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSettingsTab('providers')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${settingsTab === '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>
|
||||
<button
|
||||
onClick={() => setSettingsTab('system')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${settingsTab === 'system' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||
|
|
@ -45,7 +37,6 @@ export function SettingsLayout({ settingsTab, setSettingsTab }: SettingsLayoutPr
|
|||
{/* Settings Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{settingsTab === 'users' && <UsersSettings />}
|
||||
{settingsTab === 'providers' && <ProvidersSettings />}
|
||||
{settingsTab === 'system' && <SystemSettings />}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,36 @@
|
|||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Globe, Server, Save } from 'lucide-react';
|
||||
|
||||
export function SystemSettings() {
|
||||
const [language, setLanguage] = useState(localStorage.getItem('language') || 'English');
|
||||
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'Light');
|
||||
const [debugMode, setDebugMode] = useState(true);
|
||||
|
||||
const handleSave = () => {
|
||||
localStorage.setItem('language', language);
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
// Apply theme
|
||||
if (theme === 'Dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
// In a real app, you would dispatch a language change event or context update here
|
||||
alert(`Settings saved!\nLanguage: ${language}\nTheme: ${theme}`);
|
||||
};
|
||||
|
||||
// Initialize theme on mount if needed
|
||||
useEffect(() => {
|
||||
if (theme === 'Dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-6">
|
||||
|
|
@ -18,17 +47,25 @@ export function SystemSettings() {
|
|||
<div className="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">System Language</label>
|
||||
<select className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all">
|
||||
<option>English</option>
|
||||
<option>简体中文</option>
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
||||
>
|
||||
<option value="English">English</option>
|
||||
<option value="简体中文">简体中文</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Theme</label>
|
||||
<select className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all">
|
||||
<option>Light</option>
|
||||
<option>Dark</option>
|
||||
<option>System Default</option>
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => setTheme(e.target.value)}
|
||||
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
||||
>
|
||||
<option value="Light">Light</option>
|
||||
<option value="Dark">Dark</option>
|
||||
<option value="System Default">System Default</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -40,12 +77,14 @@ export function SystemSettings() {
|
|||
Cluster & Runtime
|
||||
</h4>
|
||||
<div className="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Max Concurrent Workflows</label>
|
||||
<input type="number" defaultValue={10} className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all" />
|
||||
</div>
|
||||
<div className="flex items-center mt-4">
|
||||
<input type="checkbox" id="debug_mode" defaultChecked className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500" />
|
||||
<input
|
||||
type="checkbox"
|
||||
id="debug_mode"
|
||||
checked={debugMode}
|
||||
onChange={(e) => setDebugMode(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="debug_mode" className="ml-2 block text-sm text-slate-700">
|
||||
Enable debug logging
|
||||
</label>
|
||||
|
|
@ -54,7 +93,10 @@ export function SystemSettings() {
|
|||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button className="flex items-center px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors shadow-sm">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="flex items-center px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors shadow-sm cursor-pointer"
|
||||
>
|
||||
<Save size={16} className="mr-2" />
|
||||
Save Changes
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -36,5 +36,5 @@ async def update_cluster_state(websocket: WebSocket):
|
|||
]
|
||||
await websocket.send_json(payload)
|
||||
await asyncio.sleep(10)
|
||||
except WebSocketDisconnect:
|
||||
except (WebSocketDisconnect, RuntimeError):
|
||||
pass
|
||||
|
|
@ -29,6 +29,6 @@ async def get_workflow(websocket: WebSocket, event_id: str):
|
|||
await websocket.send_text(await global_state_machine.get_pending.remote(event_id))
|
||||
response = await websocket.receive_text()
|
||||
await global_state_machine.put_received(event_id, response)
|
||||
except WebSocketDisconnect:
|
||||
except (WebSocketDisconnect, RuntimeError):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ class WorkflowEngine:
|
|||
consciousness_node=None,
|
||||
control_node=None,
|
||||
supervisory_node=None):
|
||||
from pretor.utils.logger import get_logger
|
||||
self.logger = get_logger('workflow_runner')
|
||||
self.workflow: PretorWorkflow = workflow
|
||||
"""工作流:当前WorkflowEngine待执行的workflow"""
|
||||
self._steps_by_id: Dict[int, WorkStep] = {step.step: step for step in self.workflow.work_link}
|
||||
|
|
|
|||
Loading…
Reference in New Issue