332 lines
14 KiB
TypeScript
332 lines
14 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import apiClient from '../../api/client';
|
|
import { Save, Plus, Edit2, Trash2, X } from 'lucide-react';
|
|
import type { Provider } from '../../types';
|
|
|
|
interface WorkerIndividual {
|
|
agent_id: string;
|
|
agent_name: string;
|
|
agent_type: string;
|
|
description?: string;
|
|
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
|
|
}
|
|
|
|
export function WorkerIndividualSettings() {
|
|
const [providers, setProviders] = useState<Provider[]>([]);
|
|
const [workers, setWorkers] = useState<WorkerIndividual[]>([]);
|
|
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 fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [provRes, workRes] = await Promise.all([
|
|
apiClient.get('/api/v1/provider/list'),
|
|
apiClient.get('/api/v1/agent/worker')
|
|
]);
|
|
setProviders(Object.values(provRes.data.provider_list || {}));
|
|
setWorkers(workRes.data.workers || []);
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
setError('Failed to load data');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
const handleEdit = (worker: any) => { // Accept the backend object which might have objects instead of strings
|
|
setEditData({
|
|
...worker,
|
|
output_template: typeof worker.output_template === 'string' ? worker.output_template : JSON.stringify(worker.output_template || {}),
|
|
bound_skill: typeof worker.bound_skill === 'string' ? worker.bound_skill : JSON.stringify(worker.bound_skill || {}),
|
|
workspace: typeof worker.workspace === 'string' ? worker.workspace : JSON.stringify(worker.workspace || [])
|
|
});
|
|
setIsNew(false);
|
|
setIsEditing(true);
|
|
setModalMessage('');
|
|
};
|
|
|
|
const handleAddNew = () => {
|
|
setEditData({
|
|
agent_name: '',
|
|
agent_type: 'OrdinaryIndividual',
|
|
description: '',
|
|
provider_title: providers.length > 0 ? providers[0].provider_title : '',
|
|
model_id: '',
|
|
system_prompt: '',
|
|
output_template: '{}',
|
|
bound_skill: '{}',
|
|
workspace: '[]'
|
|
});
|
|
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');
|
|
}
|
|
};
|
|
|
|
const handleModalSave = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setModalMessage('');
|
|
try {
|
|
const payload = {
|
|
...editData,
|
|
output_template: JSON.parse(editData.output_template || '{}'),
|
|
bound_skill: JSON.parse(editData.bound_skill || '{}'),
|
|
workspace: JSON.parse(editData.workspace || '[]')
|
|
};
|
|
|
|
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');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-5xl space-y-6 relative">
|
|
<div className="mb-8 flex justify-between items-end">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-800">Worker Individuals</h1>
|
|
<p className="text-slate-500 mt-1">Manage all system nodes and custom workers.</p>
|
|
</div>
|
|
<button
|
|
onClick={handleAddNew}
|
|
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
|
>
|
|
<Plus size={16} className="mr-2" />
|
|
Add Worker
|
|
</button>
|
|
</div>
|
|
|
|
{error && <div className="text-red-600">{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 ? (
|
|
<div className="p-6 text-slate-500">No workers 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>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{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-indigo-600 hover:bg-indigo-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>
|
|
</div>
|
|
|
|
{/* Edit/Create 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">{isNew ? 'Create Worker' : 'Edit Worker'}</h2>
|
|
<button onClick={() => setIsEditing(false)} className="text-slate-400 hover:text-slate-600">
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleModalSave} className="p-6 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-indigo-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Type</label>
|
|
<select
|
|
value={editData.agent_type || 'OrdinaryIndividual'}
|
|
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-indigo-500"
|
|
>
|
|
<option value="supervisory_node">Supervisory Node</option>
|
|
<option value="consciousness_node">Consciousness Node</option>
|
|
<option value="control_node">Control Node</option>
|
|
<option value="OrdinaryIndividual">Ordinary Individual</option>
|
|
<option value="SkillIndividual">Skill Individual</option>
|
|
<option value="SpecialIndividual">Special Individual</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-indigo-500"
|
|
>
|
|
<option value="" disabled>Select Provider</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>
|
|
{(() => {
|
|
const selectedProvider = providers.find(p => p.provider_title === editData.provider_title);
|
|
const models = selectedProvider?.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-indigo-500"
|
|
>
|
|
<option value="" disabled>Select a model</option>
|
|
{models.map(m => <option key={m} value={m}>{m}</option>)}
|
|
</select>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
|
|
<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-indigo-500"
|
|
/>
|
|
</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-indigo-500 font-mono text-sm"
|
|
/>
|
|
</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-indigo-500 font-mono text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Bound Skill (JSON)</label>
|
|
<textarea
|
|
value={editData.bound_skill || '{}'}
|
|
onChange={(e) => setEditData({...editData, bound_skill: e.target.value})}
|
|
rows={3}
|
|
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
|
|
/>
|
|
</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-indigo-500 font-mono text-sm"
|
|
/>
|
|
</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-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
|
>
|
|
<Save size={16} className="mr-2" />
|
|
Save Worker
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|