From 9ace4d499664571379eb0eb20ca935f46dd98841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=9D=E5=A4=95?= Date: Fri, 24 Apr 2026 23:34:49 +0800 Subject: [PATCH] fix: resolve provider owner type, resource UI, and db ready check (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ [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> * fix: resolve provider owner type, resource UI, and db ready check This commit fixes the following issues: 1. `provider_owner` type bug: Changed type from `int` to `str` in DB models and Pydantic schemas. 2. Frontend Provider Dropdown: `WorkerIndividualSettings.tsx` now uses a dropdown to select a created provider instead of a free-form input field. 3. Database Initialization Sync: Added an `asyncio.Event()` to `postgres.py` to prevent any DB actions from executing before `init_db()` is complete. 4. Resource Management UI: Added new pages `SkillSettings.tsx` and `WorkflowTemplateSettings.tsx` to handle frontend requests to manage skills and workflow templates. 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 | 285 ++++++++++++++++++ frontend/src/components/Auth/AuthPage.tsx | 123 ++++++++ frontend/src/components/Layout/Sidebar.tsx | 16 +- .../components/Resource/ResourceLayout.tsx | 52 ++++ .../components/Resource/ResourceSettings.tsx | 13 + .../src/components/Resource/SkillSettings.tsx | 160 ++++++++++ .../Resource/WorkflowTemplateSettings.tsx | 150 +++++++++ .../components/Settings/SettingsLayout.tsx | 11 +- .../components/Settings/SystemSettings.tsx | 68 ++++- pretor/api/cluster.py | 2 +- pretor/api/workflow.py | 2 +- pretor/core/database/postgres.py | 8 + pretor/core/database/table/provider.py | 2 +- .../model_provider/base_provider.py | 4 +- pretor/core/workflow/workflow_runner.py | 2 + .../model_provider/base_provider_test.py | 2 +- 20 files changed, 951 insertions(+), 32 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 create mode 100644 frontend/src/components/Resource/WorkflowTemplateSettings.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..c394e07 --- /dev/null +++ b/frontend/src/components/Agent/WorkerIndividualSettings.tsx @@ -0,0 +1,285 @@ +import { useState, useEffect } from 'react'; +import apiClient from '../../api/client'; +import { Bot, Save } from 'lucide-react'; +import type { Provider } from '../../types'; + +function WorkerIndividualForm({ providers }: { providers: Provider[] }) { + const [formData, setFormData] = useState({ + agent_name: '', + agent_type: 'OrdinaryIndividual', + description: '', + provider_title: providers.length > 0 ? providers[0].provider_title : '', + model_id: '', + system_prompt: '', + output_template: '{}', + bound_skill: '{}', + workspace: '[]' + }); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(''); + + // Update initial provider_title when providers load + useEffect(() => { + if (providers.length > 0 && !formData.provider_title) { + setFormData(prev => ({ ...prev, provider_title: providers[0].provider_title })); + } + }, [providers, formData.provider_title]); + + 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 ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +