From 3a96f287c7b896b51b96b4e5b8259c7a4c752fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=9D=E5=A4=95?= Date: Fri, 24 Apr 2026 18:22:39 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[Feature]=20Add=20frontend=20authen?= =?UTF-8?q?tication=20page=20and=20401=20error=20interceptor=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- frontend/src/App.tsx | 26 +- frontend/src/api/client.ts | 14 ++ frontend/src/components/Agent/AgentLayout.tsx | 43 ++++ .../{Settings => Agent}/ProvidersSettings.tsx | 0 .../Agent/WorkerIndividualSettings.tsx | 234 ++++++++++++++++++ frontend/src/components/Auth/AuthPage.tsx | 123 +++++++++ frontend/src/components/Layout/Sidebar.tsx | 16 +- .../components/Resource/ResourceLayout.tsx | 43 ++++ .../components/Resource/ResourceSettings.tsx | 13 + .../src/components/Resource/SkillSettings.tsx | 13 + .../components/Settings/SettingsLayout.tsx | 11 +- .../components/Settings/SystemSettings.tsx | 68 ++++- pretor/api/cluster.py | 2 +- pretor/api/workflow.py | 2 +- pretor/core/workflow/workflow_runner.py | 2 + 15 files changed, 582 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/Agent/AgentLayout.tsx rename frontend/src/components/{Settings => Agent}/ProvidersSettings.tsx (100%) create mode 100644 frontend/src/components/Agent/WorkerIndividualSettings.tsx create mode 100644 frontend/src/components/Auth/AuthPage.tsx create mode 100644 frontend/src/components/Resource/ResourceLayout.tsx create mode 100644 frontend/src/components/Resource/ResourceSettings.tsx create mode 100644 frontend/src/components/Resource/SkillSettings.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 34d6bef..9ac086f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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(null); + useEffect(() => { + // Check if token exists in localStorage on mount + const token = localStorage.getItem('token'); + if (token) { + setIsAuthenticated(true); + } + }, []); + + if (!isAuthenticated) { + return setIsAuthenticated(true)} />; + } + return (
@@ -22,6 +40,10 @@ function App() { {/* Main Content Area depending on view */} {currentView === 'monitoring' ? ( + ) : currentView === 'agent' ? ( + + ) : currentView === 'resource' ? ( + ) : currentView === 'dashboard' ? ( <> {/* 2. Left Panel - Cluster Status & Workflows/Chats */} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index ce2a137..7c13b50 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -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; diff --git a/frontend/src/components/Agent/AgentLayout.tsx b/frontend/src/components/Agent/AgentLayout.tsx new file mode 100644 index 0000000..3d351b9 --- /dev/null +++ b/frontend/src/components/Agent/AgentLayout.tsx @@ -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 ( +
+ {/* Agent Inner Sidebar */} +
+
+

Agents

+
+
+ + +
+
+ + {/* Agent Main Content */} +
+ {agentTab === 'worker' && } + {agentTab === 'providers' && } +
+
+ ); +} diff --git a/frontend/src/components/Settings/ProvidersSettings.tsx b/frontend/src/components/Agent/ProvidersSettings.tsx similarity index 100% rename from frontend/src/components/Settings/ProvidersSettings.tsx rename to frontend/src/components/Agent/ProvidersSettings.tsx diff --git a/frontend/src/components/Agent/WorkerIndividualSettings.tsx b/frontend/src/components/Agent/WorkerIndividualSettings.tsx new file mode 100644 index 0000000..bba324b --- /dev/null +++ b/frontend/src/components/Agent/WorkerIndividualSettings.tsx @@ -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) => { + 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 ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +