feat(frontend):优化前端页面设计
This commit is contained in:
@@ -1,38 +1,37 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '../../store/useAppStore';
|
||||
import { ProvidersSettings } from './ProvidersSettings';
|
||||
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
|
||||
|
||||
interface AgentLayoutProps {
|
||||
agentTab: string;
|
||||
setAgentTab: (tab: string) => void;
|
||||
}
|
||||
export function AgentLayout() {
|
||||
const { t } = useTranslation();
|
||||
const { innerAgentTab, setInnerAgentTab } = useAppStore();
|
||||
|
||||
const tabs = [
|
||||
{ key: 'worker', label: t('agent.individual') },
|
||||
{ key: 'providers', label: t('agent.providerManagement') },
|
||||
];
|
||||
|
||||
export function AgentLayout({ agentTab, setAgentTab }: AgentLayoutProps) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
|
||||
{/* Top Tabs for Agent Module */}
|
||||
<div className="h-14 border-b border-slate-200 bg-white flex items-center px-6 shadow-sm z-10 shrink-0 space-x-6">
|
||||
<button
|
||||
onClick={() => setAgentTab('worker')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
agentTab === 'worker' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Individual
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setAgentTab('providers')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
agentTab === 'providers' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Provider Management
|
||||
</button>
|
||||
<div className="flex-1 flex flex-col bg-bg-secondary overflow-hidden">
|
||||
<div className="h-12 border-b border-border-primary bg-bg-card/80 backdrop-blur flex items-center px-6 shrink-0 gap-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setInnerAgentTab(tab.key)}
|
||||
className={`px-4 py-2 text-xs font-semibold rounded-lg transition-all ${
|
||||
innerAgentTab === tab.key
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'text-text-muted hover:text-text-secondary hover:bg-bg-hover'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{agentTab === 'worker' && <WorkerIndividualSettings />}
|
||||
{agentTab === 'providers' && <ProvidersSettings />}
|
||||
{innerAgentTab === 'worker' && <WorkerIndividualSettings />}
|
||||
{innerAgentTab === 'providers' && <ProvidersSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Plus, X } from 'lucide-react';
|
||||
import { Box, Plus, X, Server } from 'lucide-react';
|
||||
import type { Provider } from '../../types';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
@@ -7,12 +7,7 @@ export function ProvidersSettings() {
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
provider_type: 'openai',
|
||||
provider_title: '',
|
||||
provider_url: '',
|
||||
provider_apikey: ''
|
||||
});
|
||||
const [formData, setFormData] = useState({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' });
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -20,9 +15,7 @@ export function ProvidersSettings() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/provider/list');
|
||||
const data = response.data.provider_list || {};
|
||||
const providerArray: Provider[] = Object.values(data);
|
||||
setProviders(providerArray);
|
||||
setProviders(Object.values(response.data.provider_list || {}));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch providers", error);
|
||||
setProviders([]);
|
||||
@@ -31,32 +24,7 @@ export function ProvidersSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
fetchProviders();
|
||||
}, []);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setFormData({
|
||||
provider_type: 'openai',
|
||||
provider_title: '',
|
||||
provider_url: '',
|
||||
provider_apikey: ''
|
||||
});
|
||||
setError('');
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
useEffect(() => { fetchProviders(); }, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -64,15 +32,13 @@ export function ProvidersSettings() {
|
||||
setError('Please fill in all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
await apiClient.post('/api/v1/provider', formData);
|
||||
await fetchProviders();
|
||||
handleCloseModal();
|
||||
setIsModalOpen(false);
|
||||
} catch (err) {
|
||||
console.error("Error adding provider", err);
|
||||
setError('Failed to add provider. Please check your inputs and try again.');
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
@@ -80,174 +46,97 @@ export function ProvidersSettings() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-slate-800">Provider Management</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">Configure external AI model providers and API keys.</p>
|
||||
<h3 className="text-lg font-bold text-text-primary">Provider Management</h3>
|
||||
<p className="text-sm text-text-muted mt-0.5">Configure external AI model providers</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOpenModal}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors shadow-sm cursor-pointer"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Add Provider
|
||||
<button onClick={() => { setFormData({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' }); setError(''); setIsModalOpen(true); }}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
|
||||
<Plus size={14} /> Add Provider
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center text-slate-500 py-8">Loading providers...</div>
|
||||
<div className="text-center text-text-muted py-12 text-sm">Loading providers...</div>
|
||||
) : providers.length === 0 ? (
|
||||
<div className="text-center text-slate-500 py-8 bg-white rounded-xl border border-slate-200">
|
||||
No providers configured yet. Click "Add Provider" to get started.
|
||||
<div className="text-center text-text-muted py-12 bg-bg-card rounded-2xl border border-border-primary border-dashed text-sm">
|
||||
No providers configured yet
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{providers.map((provider, i) => (
|
||||
<div key={i} className="bg-white border border-slate-200 p-5 rounded-xl shadow-sm hover:border-blue-200 transition-colors flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-50 border border-slate-100 flex items-center justify-center mr-3">
|
||||
<Box size={20} className="text-slate-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-slate-800">{provider.provider_title}</h4>
|
||||
<span className="text-xs text-slate-500 font-mono uppercase">{provider.provider_type}</span>
|
||||
</div>
|
||||
<div key={i} className="bg-bg-card border border-border-primary rounded-2xl p-5 card-hover">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center">
|
||||
<Box size={18} className="text-text-secondary" />
|
||||
</div>
|
||||
<span className={`flex items-center text-xs font-medium px-2 py-1 rounded-md border ${provider.status === 'Connected' ? 'bg-green-50 text-green-700 border-green-200' : 'bg-slate-50 text-slate-500 border-slate-200'}`}>
|
||||
{provider.status === 'Connected' && <span className="w-1.5 h-1.5 rounded-full bg-green-500 mr-1.5"></span>}
|
||||
{provider.status || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-slate-600 mb-1">URL / Endpoint:</p>
|
||||
<div className="bg-slate-50 border border-slate-100 rounded text-sm px-3 py-1.5 font-mono text-slate-700 truncate" title={provider.provider_url}>
|
||||
{provider.provider_url || 'Default'}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm text-text-primary">{provider.provider_title}</h4>
|
||||
<span className="text-[10px] text-text-muted font-mono uppercase">{provider.provider_type}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${provider.status === 'Connected' ? 'bg-success-bg text-success border-success/20' : 'bg-bg-secondary text-text-muted border-border-primary'}`}>
|
||||
{provider.status === 'Connected' && <span className="w-1 h-1 rounded-full bg-success" />}
|
||||
{provider.status || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2 mt-2">
|
||||
<button className="px-3 py-1.5 text-sm font-medium text-slate-600 bg-white border border-slate-200 rounded hover:bg-slate-50 transition-colors cursor-pointer">Edit</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!confirm('Are you sure you want to delete this provider?')) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/provider/${provider.provider_title}`);
|
||||
fetchProviders();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete provider', err);
|
||||
alert('Failed to delete provider');
|
||||
}
|
||||
}}
|
||||
className="px-3 py-1.5 text-sm font-medium text-red-600 bg-white border border-slate-200 rounded hover:bg-red-50 transition-colors cursor-pointer"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<div className="bg-bg-secondary rounded-lg px-3 py-2 mb-4">
|
||||
<p className="text-[10px] text-text-muted mb-0.5">Endpoint</p>
|
||||
<p className="text-xs font-mono text-text-secondary truncate">{provider.provider_url || 'Default'}</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">Edit</button>
|
||||
<button onClick={async () => {
|
||||
if (!confirm('Delete this provider?')) return;
|
||||
try { await apiClient.delete(`/api/v1/provider/${provider.provider_title}`); fetchProviders(); } catch { alert('Failed'); }
|
||||
}} className="px-3 py-1.5 text-xs font-medium text-danger bg-danger-bg hover:bg-danger-bg/80 rounded-lg transition-colors border border-danger/20">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Provider Modal */}
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm transition-opacity">
|
||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex justify-between items-center p-5 border-b border-slate-100">
|
||||
<h3 className="text-lg font-semibold text-slate-800">Add New Provider</h3>
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="text-slate-400 hover:text-slate-600 p-1 rounded-md transition-colors cursor-pointer"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-border-primary animate-fade-in-scale">
|
||||
<div className="flex justify-between items-center p-5 border-b border-border-primary">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server size={16} className="text-accent" />
|
||||
<h3 className="text-base font-bold text-text-primary">Add New Provider</h3>
|
||||
</div>
|
||||
<button onClick={() => setIsModalOpen(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={18} /></button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 text-red-600 text-sm rounded-lg border border-red-100">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-5 space-y-4">
|
||||
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Provider Type</label>
|
||||
<select
|
||||
name="provider_type"
|
||||
value={formData.provider_type}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all cursor-pointer"
|
||||
>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Type</label>
|
||||
<select name="provider_type" value={formData.provider_type} onChange={(e) => setFormData({...formData, provider_type: e.target.value})}
|
||||
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary">
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="deepseek">DeepSeek</option>
|
||||
<option value="claude">Claude</option>
|
||||
<option value="local">Local</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Provider Title</label>
|
||||
<input
|
||||
type="text"
|
||||
name="provider_title"
|
||||
placeholder="e.g. My OpenAI Instance"
|
||||
value={formData.provider_title}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-white border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Base URL</label>
|
||||
<input
|
||||
type="url"
|
||||
name="provider_url"
|
||||
placeholder="e.g. https://api.openai.com/v1"
|
||||
value={formData.provider_url}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-white border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
name="provider_apikey"
|
||||
placeholder="sk-..."
|
||||
value={formData.provider_apikey}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-white border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all placeholder:text-slate-400 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseModal}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-600 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitLoading}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors cursor-pointer disabled:opacity-70 flex items-center"
|
||||
>
|
||||
{submitLoading ? (
|
||||
<span className="flex items-center">
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Saving...
|
||||
</span>
|
||||
) : (
|
||||
'Add Provider'
|
||||
)}
|
||||
</button>
|
||||
{['provider_title', 'provider_url', 'provider_apikey'].map((field) => (
|
||||
<div key={field}>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{field === 'provider_title' ? 'Title' : field === 'provider_url' ? 'Base URL' : 'API Key'}
|
||||
</label>
|
||||
<input type={field === 'provider_apikey' ? 'password' : field === 'provider_url' ? 'url' : 'text'}
|
||||
name={field} value={(formData as any)[field]}
|
||||
onChange={(e) => setFormData({...formData, [field]: e.target.value})}
|
||||
placeholder={field === 'provider_title' ? 'My OpenAI' : field === 'provider_url' ? 'https://api.openai.com/v1' : 'sk-...'}
|
||||
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono" />
|
||||
</div>
|
||||
))}
|
||||
<div className="pt-2 flex justify-end gap-2">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">Cancel</button>
|
||||
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">{submitLoading ? 'Saving...' : 'Add Provider'}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import apiClient from '../../api/client';
|
||||
import { Save, Plus, Edit2, Trash2, X } from 'lucide-react';
|
||||
import { Save, Plus, Edit2, Trash2, X, Bot } from 'lucide-react';
|
||||
import type { Provider } from '../../types';
|
||||
|
||||
interface WorkerIndividual {
|
||||
@@ -11,10 +11,10 @@ interface WorkerIndividual {
|
||||
provider_title: string;
|
||||
model_id: string;
|
||||
system_prompt?: string;
|
||||
output_template?: string; // Change to string for the form state
|
||||
bound_skill?: string; // Change to string for the form state
|
||||
workspace?: string; // Change to string for the form state
|
||||
tools?: string; // Form state for tools JSON array
|
||||
output_template?: string;
|
||||
bound_skill?: string;
|
||||
workspace?: string;
|
||||
tools?: string;
|
||||
}
|
||||
|
||||
export function WorkerIndividualSettings() {
|
||||
@@ -25,12 +25,11 @@ export function WorkerIndividualSettings() {
|
||||
const [availableTools, setAvailableTools] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editData, setEditData] = useState<Partial<WorkerIndividual>>({});
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
|
||||
const [modalMessage, setModalMessage] = useState('');
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
@@ -44,43 +43,34 @@ export function WorkerIndividualSettings() {
|
||||
]);
|
||||
setProviders(Object.values(provRes.data.provider_list || {}));
|
||||
setWorkers(workRes.data.workers || []);
|
||||
|
||||
const allTools = toolsRes.data.tools || [];
|
||||
setAvailableTools(allTools);
|
||||
setAvailableTools(toolsRes.data.tools || []);
|
||||
setAvailableSkills(Object.keys(skillsRes.data.skills || {}));
|
||||
|
||||
const sysNodesData = sysRes.data.system_nodes || [];
|
||||
const defaultSysNodes = ['regulatory_node', 'consciousness_node', 'control_node'];
|
||||
|
||||
const providersList = Object.values(provRes.data.provider_list || {}) as Provider[];
|
||||
const defaultProvider = providersList.length > 0 ? providersList[0].provider_title : '';
|
||||
const sysNodesData = sysRes.data.system_nodes || [];
|
||||
const defaultSysNodes = ['regulatory_node', 'consciousness_node', 'control_node'];
|
||||
|
||||
const formattedSysNodes = defaultSysNodes.map(nodeName => {
|
||||
setSystemNodes(defaultSysNodes.map(nodeName => {
|
||||
const found = sysNodesData.find((n: any) => n.node_name === nodeName);
|
||||
return {
|
||||
agent_id: nodeName,
|
||||
agent_name: nodeName,
|
||||
agent_type: 'System Node',
|
||||
provider_title: found && found.provider_title ? found.provider_title : defaultProvider,
|
||||
model_id: found && found.model_id ? found.model_id : '',
|
||||
tools: found && found.tools ? JSON.stringify(found.tools) : '[]',
|
||||
agent_id: nodeName, agent_name: nodeName, agent_type: 'System Node',
|
||||
provider_title: found?.provider_title || defaultProvider,
|
||||
model_id: found?.model_id || '',
|
||||
tools: found?.tools ? JSON.stringify(found.tools) : '[]',
|
||||
is_system: true
|
||||
};
|
||||
});
|
||||
setSystemNodes(formattedSysNodes);
|
||||
}));
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setError('Failed to load data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
useEffect(() => { fetchData(); }, []);
|
||||
|
||||
const handleEdit = (worker: any) => { // Accept the backend object which might have objects instead of strings
|
||||
const handleEdit = (worker: any) => {
|
||||
setEditData({
|
||||
...worker,
|
||||
output_template: typeof worker.output_template === 'string' ? worker.output_template : JSON.stringify(worker.output_template || {}),
|
||||
@@ -94,46 +84,31 @@ export function WorkerIndividualSettings() {
|
||||
};
|
||||
|
||||
const handleAddNew = () => {
|
||||
setEditData({
|
||||
agent_name: '',
|
||||
agent_type: 'ordinary_individual',
|
||||
description: '',
|
||||
provider_title: providers.length > 0 ? providers[0].provider_title : '',
|
||||
model_id: '',
|
||||
system_prompt: '',
|
||||
output_template: '{}',
|
||||
bound_skill: '{}',
|
||||
workspace: '[]',
|
||||
tools: '[]'
|
||||
});
|
||||
setEditData({ agent_name: '', agent_type: 'ordinary_individual', description: '',
|
||||
provider_title: providers.length > 0 ? providers[0].provider_title : '', model_id: '',
|
||||
system_prompt: '', output_template: '{}', bound_skill: '{}', workspace: '[]', tools: '[]' });
|
||||
setIsNew(true);
|
||||
setIsEditing(true);
|
||||
setModalMessage('');
|
||||
};
|
||||
|
||||
const handleDelete = async (agent_id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this agent?')) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/agent/worker/${agent_id}`);
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
alert('Failed to delete agent');
|
||||
}
|
||||
if (!confirm('Delete this agent?')) return;
|
||||
try { await apiClient.delete(`/api/v1/agent/worker/${agent_id}`); fetchData(); } catch { alert('Failed'); }
|
||||
};
|
||||
|
||||
const handleModalSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setModalMessage('');
|
||||
setSubmitLoading(true);
|
||||
try {
|
||||
if ((editData as any).is_system) {
|
||||
const payload = {
|
||||
await apiClient.post('/api/v1/agent', {
|
||||
individual_name: editData.agent_name,
|
||||
provider_title: editData.provider_title,
|
||||
model_id: editData.model_id,
|
||||
tools: JSON.parse(editData.tools || '[]')
|
||||
};
|
||||
await apiClient.post('/api/v1/agent', payload);
|
||||
});
|
||||
} else {
|
||||
const payload = {
|
||||
...editData,
|
||||
@@ -142,305 +117,208 @@ export function WorkerIndividualSettings() {
|
||||
workspace: JSON.parse(editData.workspace || '[]'),
|
||||
tools: JSON.parse(editData.tools || '[]')
|
||||
};
|
||||
|
||||
if (isNew) {
|
||||
await apiClient.post('/api/v1/agent/worker', payload);
|
||||
} else {
|
||||
await apiClient.put(`/api/v1/agent/worker/${editData.agent_id}`, payload);
|
||||
}
|
||||
if (isNew) await apiClient.post('/api/v1/agent/worker', payload);
|
||||
else await apiClient.put(`/api/v1/agent/worker/${editData.agent_id}`, payload);
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setModalMessage(err.response?.data?.detail || err.message || 'Failed to save');
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeBadge = (type: string, isSystem?: boolean) => {
|
||||
if (isSystem) return <span className="px-2 py-0.5 rounded-md text-[10px] font-bold bg-accent-light text-accent uppercase tracking-wider">System</span>;
|
||||
const colors: Record<string, string> = {
|
||||
ordinary_individual: 'bg-bg-secondary text-text-muted',
|
||||
skill_individual: 'bg-success-bg text-success',
|
||||
special_individual: 'bg-warning-bg text-warning',
|
||||
};
|
||||
return <span className={`px-2 py-0.5 rounded-md text-[10px] font-medium ${colors[type] || colors.ordinary_individual}`}>{type.replace('_', ' ')}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl space-y-6 relative">
|
||||
<div className="mb-8 flex justify-between items-end">
|
||||
<div className="max-w-5xl space-y-6">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">Individual</h1>
|
||||
<p className="text-slate-500 mt-1">Manage all system nodes and custom workers.</p>
|
||||
<h1 className="text-lg font-bold text-text-primary">Individual</h1>
|
||||
<p className="text-sm text-text-muted mt-0.5">Manage system nodes and custom workers</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddNew}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Add Worker
|
||||
<button onClick={handleAddNew} className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
|
||||
<Plus size={14} /> Add Worker
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-red-600">{error}</div>}
|
||||
{error && <div className="text-sm text-danger bg-danger-bg border border-danger/20 rounded-xl p-3">{error}</div>}
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div className="p-0">
|
||||
{loading ? (
|
||||
<div className="p-6 text-slate-500">Loading...</div>
|
||||
) : (workers.length === 0 && systemNodes.length === 0) ? (
|
||||
<div className="p-6 text-slate-500">No individuals found.</div>
|
||||
) : (
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-200 text-slate-600 text-sm">
|
||||
<th className="p-4 font-semibold">Name</th>
|
||||
<th className="p-4 font-semibold">Type</th>
|
||||
<th className="p-4 font-semibold">Provider / Model ID</th>
|
||||
<th className="p-4 font-semibold text-right">Actions</th>
|
||||
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-6 text-text-muted text-sm">Loading...</div>
|
||||
) : (workers.length === 0 && systemNodes.length === 0) ? (
|
||||
<div className="p-6 text-text-muted text-sm">No individuals found.</div>
|
||||
) : (
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="bg-bg-secondary border-b border-border-primary text-text-muted text-xs uppercase tracking-wider">
|
||||
<th className="px-5 py-3 font-semibold">Name</th>
|
||||
<th className="px-5 py-3 font-semibold">Type</th>
|
||||
<th className="px-5 py-3 font-semibold">Provider / Model</th>
|
||||
<th className="px-5 py-3 font-semibold text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-secondary">
|
||||
{systemNodes.map((w) => (
|
||||
<tr key={w.agent_id} className="bg-bg-secondary/50 hover:bg-bg-hover transition-colors">
|
||||
<td className="px-5 py-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
|
||||
<Bot size={14} className="text-accent" />
|
||||
</div>
|
||||
<span className="font-medium text-text-primary text-xs">{w.agent_name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-3">{getTypeBadge(w.agent_type, true)}</td>
|
||||
<td className="px-5 py-3 text-xs text-text-muted">{w.provider_title} / {w.model_id}</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<button onClick={() => handleEdit(w)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all"><Edit2 size={14} /></button>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{systemNodes.map((w) => (
|
||||
<tr key={w.agent_id} className="border-b border-slate-100 bg-slate-50 hover:bg-slate-100 transition-colors">
|
||||
<td className="p-4 font-medium text-slate-800">{w.agent_name}</td>
|
||||
<td className="p-4 text-slate-600">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{w.agent_type}</span>
|
||||
</td>
|
||||
<td className="p-4 text-slate-600 text-sm">
|
||||
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
|
||||
</td>
|
||||
<td className="p-4 text-right space-x-2">
|
||||
<button onClick={() => handleEdit(w)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Edit">
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{workers.map((w) => (
|
||||
<tr key={w.agent_id} className="border-b border-slate-100 hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4 font-medium text-slate-800">{w.agent_name}</td>
|
||||
<td className="p-4 text-slate-600">
|
||||
<span className="px-2 py-1 bg-slate-100 rounded text-xs">{w.agent_type}</span>
|
||||
</td>
|
||||
<td className="p-4 text-slate-600 text-sm">
|
||||
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
|
||||
</td>
|
||||
<td className="p-4 text-right space-x-2">
|
||||
<button onClick={() => handleEdit(w)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Edit">
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(w.agent_id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Delete">
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{workers.map((w) => (
|
||||
<tr key={w.agent_id} className="hover:bg-bg-hover transition-colors">
|
||||
<td className="px-5 py-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-bg-secondary border border-border-primary flex items-center justify-center">
|
||||
<Bot size={14} className="text-text-muted" />
|
||||
</div>
|
||||
<span className="font-medium text-text-primary text-xs">{w.agent_name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-3">{getTypeBadge(w.agent_type)}</td>
|
||||
<td className="px-5 py-3 text-xs text-text-muted">{w.provider_title} / {w.model_id}</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<button onClick={() => handleEdit(w)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all mr-0.5"><Edit2 size={14} /></button>
|
||||
<button onClick={() => handleDelete(w.agent_id)} className="p-1.5 text-text-muted hover:text-danger hover:bg-danger-bg rounded-lg transition-all"><Trash2 size={14} /></button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Edit/Create Modal */}
|
||||
{/* Modal */}
|
||||
{isEditing && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center p-6 border-b border-slate-100 sticky top-0 bg-white z-10">
|
||||
<h2 className="text-xl font-bold text-slate-800">
|
||||
{(editData as any).is_system ? 'Edit System Node' : (isNew ? 'Create Worker' : 'Edit Worker')}
|
||||
</h2>
|
||||
<button onClick={() => setIsEditing(false)} className="text-slate-400 hover:text-slate-600">
|
||||
<X size={24} />
|
||||
</button>
|
||||
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-border-primary">
|
||||
<div className="flex justify-between items-center p-5 border-b border-border-primary sticky top-0 bg-bg-card z-10">
|
||||
<h2 className="text-base font-bold text-text-primary">{(editData as any).is_system ? 'Edit System Node' : (isNew ? 'Create Worker' : 'Edit Worker')}</h2>
|
||||
<button onClick={() => setIsEditing(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={20} /></button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleModalSave} className="p-6 space-y-4">
|
||||
<form onSubmit={handleModalSave} className="p-5 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
|
||||
type="text"
|
||||
required
|
||||
value={editData.agent_name || ''}
|
||||
onChange={(e) => setEditData({...editData, agent_name: e.target.value})}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
disabled={(editData as any).is_system}
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Name</label>
|
||||
<input type="text" required value={editData.agent_name || ''} onChange={(e) => setEditData({...editData, agent_name: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" disabled={(editData as any).is_system} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Type</label>
|
||||
<select
|
||||
value={editData.agent_type || 'ordinary_individual'}
|
||||
onChange={(e) => setEditData({...editData, agent_type: e.target.value})}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
disabled={(editData as any).is_system}
|
||||
>
|
||||
<option value="ordinary_individual">Ordinary Individual</option>
|
||||
<option value="skill_individual">Skill Individual</option>
|
||||
<option value="special_individual">Special Individual</option>
|
||||
{(editData as any).is_system && (
|
||||
<option value="System Node">System Node</option>
|
||||
)}
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Type</label>
|
||||
<select value={editData.agent_type || 'ordinary_individual'} onChange={(e) => setEditData({...editData, agent_type: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" disabled={(editData as any).is_system}>
|
||||
<option value="ordinary_individual">Ordinary</option>
|
||||
<option value="skill_individual">Skill</option>
|
||||
<option value="special_individual">Special</option>
|
||||
{(editData as any).is_system && <option value="System Node">System Node</option>}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Provider Title</label>
|
||||
<select
|
||||
value={editData.provider_title || ''}
|
||||
onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="" disabled>Select Provider</option>
|
||||
{providers.map((p) => (
|
||||
<option key={p.provider_title} value={p.provider_title}>{p.provider_title}</option>
|
||||
))}
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Provider</label>
|
||||
<select value={editData.provider_title || ''} onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})} required
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
|
||||
<option value="" disabled>Select</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>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Model</label>
|
||||
{(() => {
|
||||
const selectedProvider = providers.find(p => p.provider_title === editData.provider_title);
|
||||
const models = selectedProvider?.provider_models || [];
|
||||
const sp = providers.find(p => p.provider_title === editData.provider_title);
|
||||
const models = sp?.provider_models || [];
|
||||
return (
|
||||
<select
|
||||
value={editData.model_id || ''}
|
||||
onChange={(e) => setEditData({...editData, model_id: e.target.value})}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="" disabled>Select a model</option>
|
||||
<select value={editData.model_id || ''} onChange={(e) => setEditData({...editData, model_id: e.target.value})} required
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
|
||||
<option value="" disabled>Select</option>
|
||||
{models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!(editData as any).is_system && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Description</label>
|
||||
<textarea
|
||||
value={editData.description || ''}
|
||||
onChange={(e) => setEditData({...editData, description: e.target.value})}
|
||||
rows={2}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Description</label>
|
||||
<textarea value={editData.description || ''} onChange={(e) => setEditData({...editData, description: e.target.value})} rows={2}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">System Prompt</label>
|
||||
<textarea
|
||||
value={editData.system_prompt || ''}
|
||||
onChange={(e) => setEditData({...editData, system_prompt: e.target.value})}
|
||||
rows={3}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">System Prompt</label>
|
||||
<textarea value={editData.system_prompt || ''} onChange={(e) => setEditData({...editData, system_prompt: e.target.value})} rows={3}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
|
||||
<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">Output Template (JSON)</label>
|
||||
<textarea
|
||||
value={editData.output_template || '{}'}
|
||||
onChange={(e) => setEditData({...editData, output_template: e.target.value})}
|
||||
rows={3}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Output Template (JSON)</label>
|
||||
<textarea value={editData.output_template || '{}'} onChange={(e) => setEditData({...editData, output_template: e.target.value})} rows={3}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Bound Skill (Select)</label>
|
||||
<select
|
||||
value={(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(editData.bound_skill || '{}');
|
||||
return Object.keys(parsed)[0] || '';
|
||||
} catch { return ''; }
|
||||
})()}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
const newSkill = val ? { [val]: [] } : {};
|
||||
setEditData({...editData, bound_skill: JSON.stringify(newSkill)});
|
||||
}}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
disabled={editData.agent_type !== 'skill_individual'}
|
||||
>
|
||||
<option value="">No Skill Bound</option>
|
||||
{availableSkills.map(skill => (
|
||||
<option key={skill} value={skill}>{skill}</option>
|
||||
))}
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Bound Skill</label>
|
||||
<select value={(() => { try { return Object.keys(JSON.parse(editData.bound_skill || '{}'))[0] || ''; } catch { return ''; } })()}
|
||||
onChange={(e) => setEditData({...editData, bound_skill: JSON.stringify(e.target.value ? { [e.target.value]: [] } : {})})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" disabled={editData.agent_type !== 'skill_individual'}>
|
||||
<option value="">None</option>
|
||||
{availableSkills.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Workspace (JSON Array)</label>
|
||||
<textarea
|
||||
value={editData.workspace || '[]'}
|
||||
onChange={(e) => setEditData({...editData, workspace: e.target.value})}
|
||||
rows={2}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Workspace (JSON)</label>
|
||||
<textarea value={editData.workspace || '[]'} onChange={(e) => setEditData({...editData, workspace: e.target.value})} rows={2}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Tools (Select Multiple)</label>
|
||||
<div className="flex flex-wrap gap-2 p-4 border border-slate-200 rounded-lg max-h-48 overflow-y-auto">
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Tools</label>
|
||||
<div className="flex flex-wrap gap-1.5 p-3 bg-bg-input border border-border-primary rounded-xl max-h-40 overflow-y-auto">
|
||||
{availableTools.map(tool => {
|
||||
let currentTools: string[] = [];
|
||||
try {
|
||||
currentTools = JSON.parse(editData.tools || '[]');
|
||||
} catch { currentTools = []; }
|
||||
|
||||
try { currentTools = JSON.parse(editData.tools || '[]'); } catch { }
|
||||
const isSelected = currentTools.includes(tool);
|
||||
return (
|
||||
<button
|
||||
key={tool}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let updatedTools = [...currentTools];
|
||||
if (isSelected) {
|
||||
updatedTools = updatedTools.filter(t => t !== tool);
|
||||
} else {
|
||||
updatedTools.push(tool);
|
||||
}
|
||||
setEditData({...editData, tools: JSON.stringify(updatedTools)});
|
||||
}}
|
||||
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
|
||||
isSelected
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-200'
|
||||
: 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
<button key={tool} type="button" onClick={() => {
|
||||
const updated = isSelected ? currentTools.filter(t => t !== tool) : [...currentTools, tool];
|
||||
setEditData({...editData, tools: JSON.stringify(updated)});
|
||||
}}
|
||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${isSelected ? 'bg-accent-light text-accent border border-accent/20' : 'bg-bg-secondary text-text-muted border border-border-primary hover:border-text-muted'}`}>
|
||||
{tool}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{availableTools.length === 0 && (
|
||||
<span className="text-sm text-slate-500">No tools available</span>
|
||||
)}
|
||||
{availableTools.length === 0 && <span className="text-xs text-text-muted">No tools</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modalMessage && (
|
||||
<div className="p-3 bg-red-50 text-red-700 text-sm rounded-lg">
|
||||
{modalMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 flex justify-end space-x-3 border-t border-slate-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditing(false)}
|
||||
className="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Save size={16} className="mr-2" />
|
||||
Save Worker
|
||||
{modalMessage && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{modalMessage}</div>}
|
||||
<div className="pt-3 flex justify-end gap-2 border-t border-border-primary">
|
||||
<button type="button" onClick={() => setIsEditing(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">Cancel</button>
|
||||
<button type="submit" disabled={submitLoading} className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">
|
||||
<Save size={14} /> {submitLoading ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user