Compare commits
4 Commits
dcf53524b2
...
e6fcb08ce3
| Author | SHA1 | Date |
|---|---|---|
|
|
e6fcb08ce3 | |
|
|
1df416ac4d | |
|
|
9ace4d4996 | |
|
|
3a96f287c7 |
2
.env
2
.env
|
|
@ -1,5 +1,5 @@
|
|||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_PASSWORD=postgrespassword
|
||||
POSTGRES_HOST=127.0.0.1
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=pretor
|
||||
|
|
|
|||
|
|
@ -33,5 +33,3 @@ services:
|
|||
- POSTGRES_DB=pretor
|
||||
- SECRET_KEY=changethiskey12345
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
|
|
|||
|
|
@ -2,23 +2,12 @@
|
|||
---
|
||||
## 问题栏
|
||||
#### 🔴 核心缺陷与修复 (Bug Fixes & Stability)
|
||||
- [x] /pretor/core/individual每个template进行优化
|
||||
- [x] /pretor/worker_individual待完善复合子个体和基础子个体
|
||||
|
||||
|
||||
#### 🛡️ 安全与合规 (Security & Auth)
|
||||
- [ ] 优化安全架构防止模型注入
|
||||
- [ ] 设计workflowEngine的自动扩缩容设计
|
||||
- [ ] 完善错误捕获和日志系统
|
||||
|
||||
#### ⚡ 性能与资源优化 (Performance & Scalability)
|
||||
- [ ] 增加对应全workflow的情况追踪,使得在任务运行中人机交互更加自然方便
|
||||
- [ ] 优化import
|
||||
|
||||
|
||||
#### 🏗️ 架构演进 (Architecture & Refactoring)
|
||||
- [x] ~~使用fastapi-users完善用户系统~~(2026/4/19 fastapi-users会严重摧毁代码的优雅性)
|
||||
- [x] 升级auth功能
|
||||
- [x] /pretor/api的接口函数进行重构
|
||||
- [x] /dockerfile待完善
|
||||
- [ ] 完善沙箱功能
|
||||
- [ ] 完善爬虫功能
|
||||
- [ ] 对接更多的provider
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
## Pretor项目开发
|
||||
#项目规划
|
||||
---
|
||||
#### 全局规划:
|
||||
- [ ] 实现监管模型的资源调度
|
||||
- [ ] 实现子个体的工作传递
|
||||
- [ ] 实现用户交互接口与ray集群的交互
|
||||
- [ ] 实现监管模型调度ray资源的接口
|
||||
- [ ] 实现由监管模型理解并发布,子个体向下布置任务,完成任务向上传递,监管模型检查的全工作流
|
||||
---
|
||||
|
||||
#### 简介
|
||||
**Pretor**是一款python开发,实现将小模型进行微调后整理为一个大型集群,从而实现低算力情况下高复杂度任务的实现。
|
||||
系统模型分为以下部分:
|
||||
|
|
@ -15,13 +7,6 @@
|
|||
- **管控节点**:负责调度系统资源;
|
||||
- **意识节点**:负责复杂任务的处理;
|
||||
- **生长节点**:负责获取资源并且将基础模型训练为特化模型;
|
||||
- **感知模块**:与外界交互的模型,如embedding模型,tts模型等;
|
||||
- **复合子个体**:将监管节点的任务领取并进行专业的拆解任务并进行分配;
|
||||
- **生产子个体**:领取任务最小单位并执行;
|
||||
---
|
||||
#### 短期规划
|
||||
v0.1版本
|
||||
- [ ] **workflow构建**:构建任务的工作流
|
||||
- [ ] **接口构建**:对接vllm,openai接口和gemini接口
|
||||
- [ ] **工具构建**:配置供模型调用的爬虫工具箱,docker接口
|
||||
- [ ] **平台对接构建**:对接telegram等消息平台
|
||||
- **特殊子个体**:与外界交互的模型,如embedding模型,tts模型等;
|
||||
- **专家子个体**:;
|
||||
- **基础子个体**:普通的agent对象;
|
||||
|
|
@ -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,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<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>
|
||||
<select
|
||||
required
|
||||
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"
|
||||
>
|
||||
{providers.length === 0 ? (
|
||||
<option value="" disabled>No providers available. Create one first.</option>
|
||||
) : (
|
||||
providers.map((p) => (
|
||||
<option key={p.provider_title} value={p.provider_title}>
|
||||
{p.provider_title}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</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 [providers, setProviders] = useState<Provider[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProviders = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/provider/list');
|
||||
const data = response.data.provider_list || {};
|
||||
const providerArray: Provider[] = Object.values(data);
|
||||
setProviders(providerArray);
|
||||
if (providerArray.length > 0) {
|
||||
setProviderTitle(providerArray[0].provider_title);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch providers", error);
|
||||
setProviders([]);
|
||||
}
|
||||
};
|
||||
fetchProviders();
|
||||
}, []);
|
||||
|
||||
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>
|
||||
<select
|
||||
value={providerTitle}
|
||||
onChange={(e) => setProviderTitle(e.target.value)}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
{providers.length === 0 ? (
|
||||
<option value="" disabled>No providers available. Create one first.</option>
|
||||
) : (
|
||||
providers.map((p) => (
|
||||
<option key={p.provider_title} value={p.provider_title}>
|
||||
{p.provider_title}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</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 providers={providers} />
|
||||
</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,52 @@
|
|||
import { Wrench, Database, FileCode } from 'lucide-react';
|
||||
import { SkillSettings } from './SkillSettings';
|
||||
import { ResourceSettings } from './ResourceSettings';
|
||||
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('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 === 'workflow_template' && <WorkflowTemplateSettings />}
|
||||
{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,160 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import apiClient from '../../api/client';
|
||||
import { Download, Trash2, Plus, Box } from 'lucide-react';
|
||||
|
||||
export function SkillSettings() {
|
||||
const [skills, setSkills] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [repoUrl, setRepoUrl] = useState('');
|
||||
const [path, setPath] = useState('');
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const fetchSkills = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/resource/skill');
|
||||
setSkills(response.data.skills || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch skills:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSkills();
|
||||
}, []);
|
||||
|
||||
const handleInstall = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!repoUrl) return;
|
||||
|
||||
setInstalling(true);
|
||||
setMessage('');
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await apiClient.post('/api/v1/resource/skill', {
|
||||
repo_url: repoUrl,
|
||||
path: path || null
|
||||
});
|
||||
setMessage('Skill installed successfully');
|
||||
setRepoUrl('');
|
||||
setPath('');
|
||||
fetchSkills();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setError(err.response?.data?.message || 'Failed to install skill');
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (skillName: string) => {
|
||||
if (!confirm(`Are you sure you want to delete ${skillName}?`)) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/resource/skill/${skillName}`);
|
||||
fetchSkills();
|
||||
} catch (err: any) {
|
||||
console.error('Failed to delete skill:', err);
|
||||
alert('Failed to delete skill');
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<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">
|
||||
<Download size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-800">Install Skill</h2>
|
||||
<p className="text-sm text-slate-500">Install a new skill from a repository.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleInstall} 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">Repository URL</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Path (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message && <div className="text-green-600 text-sm">{message}</div>}
|
||||
{error && <div className="text-red-600 text-sm">{error}</div>}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<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"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
{installing ? 'Installing...' : 'Install'}
|
||||
</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">Installed Skills</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{loading ? (
|
||||
<div className="text-slate-500 text-sm">Loading skills...</div>
|
||||
) : skills.length === 0 ? (
|
||||
<div className="text-slate-500 text-sm">No skills installed yet.</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{skills.map((skill) => (
|
||||
<div key={skill} className="p-4 border border-slate-200 rounded-xl flex items-center justify-between hover:shadow-sm transition-shadow">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500">
|
||||
<Box size={16} />
|
||||
</div>
|
||||
<span className="font-medium text-slate-800">{skill}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDelete(skill)}
|
||||
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Delete Skill"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import apiClient from '../../api/client';
|
||||
import { FileCode, Trash2, Plus, LayoutTemplate } from 'lucide-react';
|
||||
|
||||
interface WorkflowTemplate {
|
||||
name: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function WorkflowTemplateSettings() {
|
||||
const [templates, setTemplates] = useState<Record<string, WorkflowTemplate>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [templateJson, setTemplateJson] = useState('{\n "name": "my_template"\n}');
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/resource/workflow_template');
|
||||
setTemplates(response.data.templates || {});
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch templates:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplates();
|
||||
}, []);
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setCreating(true);
|
||||
setMessage('');
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const parsedJson = JSON.parse(templateJson);
|
||||
await apiClient.post('/api/v1/resource/workflow_template', parsedJson);
|
||||
setMessage('Workflow template created successfully');
|
||||
setTemplateJson('{\n "name": "my_template"\n}');
|
||||
fetchTemplates();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
if (err instanceof SyntaxError) {
|
||||
setError('Invalid JSON format');
|
||||
} else {
|
||||
setError(err.response?.data?.message || 'Failed to create workflow template');
|
||||
}
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (templateName: string) => {
|
||||
if (!confirm(`Are you sure you want to delete ${templateName}?`)) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/resource/workflow_template/${templateName}`);
|
||||
fetchTemplates();
|
||||
} catch (err: any) {
|
||||
console.error('Failed to delete template:', err);
|
||||
alert('Failed to delete template');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-800">Workflow Templates</h1>
|
||||
<p className="text-slate-500 mt-1">Manage and create reusable workflow templates.</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 space-x-3">
|
||||
<div className="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<FileCode size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-800">Create Template</h2>
|
||||
<p className="text-sm text-slate-500">Provide the JSON definition for a new workflow template.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Template JSON Definition</label>
|
||||
<textarea
|
||||
required
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{message && <div className="text-green-600 text-sm">{message}</div>}
|
||||
{error && <div className="text-red-600 text-sm">{error}</div>}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<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"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
{creating ? 'Creating...' : 'Create Template'}
|
||||
</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">Available Templates</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{loading ? (
|
||||
<div className="text-slate-500 text-sm">Loading templates...</div>
|
||||
) : Object.keys(templates).length === 0 ? (
|
||||
<div className="text-slate-500 text-sm">No workflow templates created yet.</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Object.keys(templates).map((name) => (
|
||||
<div key={name} className="p-4 border border-slate-200 rounded-xl flex items-center justify-between hover:shadow-sm transition-shadow">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500">
|
||||
<LayoutTemplate size={16} />
|
||||
</div>
|
||||
<span className="font-medium text-slate-800">{name}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDelete(name)}
|
||||
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Delete Template"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
|
|
|
|||
14
main.py
14
main.py
|
|
@ -15,15 +15,13 @@ import os
|
|||
|
||||
|
||||
async def start_system():
|
||||
# 1. 初始化 Ray
|
||||
db_host = os.getenv("POSTGRES_HOST", "db")
|
||||
env_vars = {
|
||||
"POSTGRES_USER": "postgres",
|
||||
"POSTGRES_PASSWORD": "postgres",
|
||||
"POSTGRES_HOST": db_host,
|
||||
"POSTGRES_PORT": "5432",
|
||||
"POSTGRES_DB": "postgres",
|
||||
"SECRET_KEY": "yoursecretkey"
|
||||
"POSTGRES_USER": os.getenv("POSTGRES_USER", "postgres"),
|
||||
"POSTGRES_PASSWORD": os.getenv("POSTGRES_PASSWORD", ""),
|
||||
"POSTGRES_HOST": os.getenv("POSTGRES_HOST", "db"),
|
||||
"POSTGRES_PORT": os.getenv("POSTGRES_PORT", "5432"),
|
||||
"POSTGRES_DB": os.getenv("POSTGRES_DB", "postgres"),
|
||||
"SECRET_KEY": os.getenv("SECRET_KEY", "secret"),
|
||||
}
|
||||
|
||||
ray.init(ignore_reinit_error=True,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
import ray
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
|
|
@ -39,6 +40,8 @@ class PostgresDatabase:
|
|||
self._provider_database = ProviderDatabase(self.async_session_maker)
|
||||
self._individual_database = IndividualDatabase(self.async_session_maker)
|
||||
|
||||
self.ready_event = asyncio.Event()
|
||||
|
||||
async def init_db(self) -> None:
|
||||
try:
|
||||
async with self.async_engine.begin() as conn:
|
||||
|
|
@ -47,15 +50,20 @@ class PostgresDatabase:
|
|||
# Provide a warning if the database is not accessible, allowing
|
||||
# the app to start up for development/UI tests without crashing immediately.
|
||||
print(f"Warning: Failed to initialize PostgreSQL database: {e}")
|
||||
finally:
|
||||
self.ready_event.set()
|
||||
|
||||
async def auth_database(self, method_name: str, *args, **kwargs):
|
||||
await self.ready_event.wait()
|
||||
method = getattr(self._auth_database, method_name)
|
||||
return await method(*args, **kwargs)
|
||||
|
||||
async def provider_database(self, method_name: str, *args, **kwargs):
|
||||
await self.ready_event.wait()
|
||||
method = getattr(self._provider_database, method_name)
|
||||
return await method(*args, **kwargs)
|
||||
|
||||
async def individual_database(self, method_name: str, *args, **kwargs):
|
||||
await self.ready_event.wait()
|
||||
method = getattr(self._individual_database, method_name)
|
||||
return await method(*args, **kwargs)
|
||||
|
|
@ -28,5 +28,5 @@ class Provider(SQLModel, table=True):
|
|||
|
||||
provider_models: List[str] = Field(sa_column=Column(JSON))
|
||||
|
||||
provider_owner: int
|
||||
provider_owner: str
|
||||
is_active: bool = Field(default=True, description="该服务商节点是否在线/启用")
|
||||
|
|
@ -27,14 +27,14 @@ class Provider(BaseModel):
|
|||
provider_apikey: str
|
||||
provider_models: List[str]
|
||||
provider_type: str
|
||||
provider_owner: int | None = None
|
||||
provider_owner: str | None = None
|
||||
provider_status: ProviderStatus = ProviderStatus.UP
|
||||
|
||||
class ProviderArgs(BaseModel):
|
||||
provider_title: str
|
||||
provider_url: str
|
||||
provider_apikey: str
|
||||
provider_owner: int
|
||||
provider_owner: str
|
||||
|
||||
class BaseProvider(ABC):
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ def test_provider_args():
|
|||
provider_title="title",
|
||||
provider_url="url",
|
||||
provider_apikey="key",
|
||||
provider_owner=1
|
||||
provider_owner="1"
|
||||
)
|
||||
assert args.provider_title == "title"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue