| <template> |
| <div class="modal-overlay" @click.self="$emit('close')"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h2 class="modal-title">Add New Agent</h2> |
| <button class="close-button" @click="$emit('close')"> |
| ✕ |
| </button> |
| </div> |
| |
| <div class="modal-body"> |
| <form @submit.prevent="handleSubmit"> |
| |
| <div v-if="loading" class="loading-overlay"> |
| <div class="loading-spinner"></div> |
| <p class="loading-text">Creating agent...</p> |
| </div> |
| |
| |
| <div v-if="error" class="error-message"> |
| <span class="error-icon">❌</span> |
| <span>{{ error }}</span> |
| </div> |
| |
| |
| <div v-if="successMessage" class="success-message"> |
| <span class="success-icon">✅</span> |
| <span>{{ successMessage }}</span> |
| </div> |
| |
| <div class="form-group"> |
| <label class="form-label">Agent Name *</label> |
| <input |
| v-model="form.name" |
| type="text" |
| class="form-input" |
| placeholder="Enter agent name" |
| required |
| :disabled="loading" |
| /> |
| </div> |
| |
| <div class="form-group"> |
| <label class="form-label">Agent Type *</label> |
| <select v-model="form.type" class="form-input" required :disabled="loading"> |
| <option value="">Select type...</option> |
| <option value="coordinator">Coordinator</option> |
| <option value="developer">Developer</option> |
| <option value="specialist">Specialist</option> |
| <option value="generalist">Generalist</option> |
| <option value="tool_user">Tool User</option> |
| <option value="monitor">Monitor</option> |
| </select> |
| </div> |
| |
| <div class="form-group"> |
| <label class="form-label">Description</label> |
| <textarea |
| v-model="form.description" |
| class="form-input resize-y" |
| rows="3" |
| placeholder="Agent description and capabilities" |
| :disabled="loading" |
| ></textarea> |
| </div> |
| |
| <div class="form-group"> |
| <label class="form-label">Capabilities (comma-separated)</label> |
| <input |
| v-model="form.capabilities" |
| type="text" |
| class="form-input" |
| placeholder="e.g., coding, debugging, analysis" |
| :disabled="loading" |
| /> |
| <small class="form-help"> |
| Available: orchestration, coordination, strategy, coding, debugging, architecture, analysis, research, reporting, medical_advice, diagnosis, treatment, legal_advice, compliance, contracts, financial_analysis, investment, budgeting |
| </small> |
| </div> |
| |
| <div class="form-actions"> |
| <button type="button" class="cancel-button" @click="$emit('close')" :disabled="loading"> |
| Cancel |
| </button> |
| <button type="submit" class="submit-button" :disabled="!isFormValid || loading"> |
| {{ loading ? 'Creating...' : 'Create Agent' }} |
| </button> |
| </div> |
| </form> |
| </div> |
| </div> |
| </div> |
| </template> |
| |
| <script> |
| import { ref, reactive, computed } from 'vue' |
| import { saapApi } from '../../services/saapApi.js' |
| |
| export default { |
| name: 'AddAgentModal', |
| emits: ['close', 'agent-created'], |
| setup(props, { emit }) { |
| const loading = ref(false) |
| const error = ref('') |
| const successMessage = ref('') |
| |
| const form = reactive({ |
| name: '', |
| type: '', |
| description: '', |
| capabilities: '' |
| }) |
| |
| const isFormValid = computed(() => { |
| return form.name.trim() && form.type.trim() |
| }) |
| |
| const handleSubmit = async () => { |
| if (!isFormValid.value || loading.value) return |
| |
| loading.value = true |
| error.value = '' |
| successMessage.value = '' |
| |
| try { |
| |
| const capabilities = validateAndCleanCapabilities(form.capabilities) |
| const agentId = generateAgentId(form.name) |
| const description = form.description.trim() || `${form.name} - ${form.type} agent` |
| |
| const agentData = { |
| |
| id: agentId, |
| name: form.name.trim(), |
| type: form.type, |
| status: "inactive", |
| description: description, |
| |
| |
| metadata: { |
| version: "1.0.0", |
| created: new Date().toISOString(), |
| updated: new Date().toISOString(), |
| creator: "user", |
| tags: [form.type, "custom_agent"] |
| }, |
| |
| |
| appearance: { |
| color: getAgentColor(form.type), |
| avatar: `/assets/agents/${agentId}.svg`, |
| display_name: form.name.trim(), |
| subtitle: `${form.type.charAt(0).toUpperCase() + form.type.slice(1)} Agent`, |
| description: description |
| }, |
| |
| |
| capabilities: capabilities, |
| |
| |
| llm_config: { |
| model: "phi3:mini", |
| temperature: 0.7, |
| max_tokens: 2048, |
| top_p: 0.9, |
| system_prompt: `You are ${form.name.trim()}, a ${form.type} agent with the following capabilities: ${capabilities.join(', ')}. ${description}`, |
| stop_sequences: [], |
| context_window: 4096 |
| }, |
| |
| |
| communication: { |
| input_queue: `${agentId}_input`, |
| output_queue: `${agentId}_output`, |
| message_types: ["request", "response"], |
| max_queue_size: 1000, |
| message_ttl: 3600, |
| priority_handling: true |
| }, |
| |
| |
| ui_components: { |
| dashboard_widget: "AgentCard", |
| detail_view: "AgentDetail", |
| configuration_form: "AgentConfig" |
| }, |
| |
| |
| custom_config: {} |
| } |
| |
| console.log('🚀 Creating agent with backend-compatible data:', agentData) |
| |
| |
| const response = await saapApi.createAgent(agentData) |
| |
| console.log('✅ Backend response:', response) |
| |
| |
| if (response.success === true && response.agent) { |
| console.log('✅ Agent created successfully (format 1):', response.agent) |
| successMessage.value = response.message || 'Agent created successfully!' |
| resetForm() |
| emit('agent-created', response.agent) |
| setTimeout(() => emit('close'), 1500) |
| |
| } else if (response.id || response.agent_id) { |
| console.log('✅ Agent created successfully (format 2):', response) |
| successMessage.value = 'Agent created successfully!' |
| resetForm() |
| emit('agent-created', response) |
| setTimeout(() => emit('close'), 1500) |
| |
| } else { |
| console.warn('⚠️ Unexpected response format:', response) |
| throw new Error('Unexpected response format from server') |
| } |
| |
| } catch (err) { |
| console.error('❌ Agent creation failed:', err) |
| |
| |
| if (err.response) { |
| const status = err.response.status |
| const data = err.response.data |
| |
| if (status === 422) { |
| error.value = `Validation error: ${data.detail || 'Invalid agent data'}` |
| } else if (status === 409) { |
| error.value = `Agent already exists: ${data.detail || 'Choose a different name'}` |
| } else if (status === 500) { |
| error.value = `Server error: ${data.detail || 'Please try again later'}` |
| } else { |
| error.value = data.detail || `HTTP ${status}: Request failed` |
| } |
| } else if (err.code === 'ECONNREFUSED' || err.message.includes('Network Error')) { |
| error.value = 'Backend server is not running. Please start the SAAP backend.' |
| } else if (err.message) { |
| error.value = err.message |
| } else { |
| error.value = 'Failed to create agent. Please check your network and try again.' |
| } |
| } finally { |
| loading.value = false |
| } |
| } |
| |
| const resetForm = () => { |
| form.name = '' |
| form.type = '' |
| form.description = '' |
| form.capabilities = '' |
| } |
| |
| |
| const generateAgentId = (name) => { |
| return name |
| .toLowerCase() |
| .replace(/[^a-z0-9\s]/g, '') |
| .replace(/\s+/g, '_') |
| .substring(0, 20) |
| } |
| |
| |
| const getAgentColor = (type) => { |
| const typeColors = { |
| 'coordinator': '#8B5CF6', |
| 'developer': '#14B8A6', |
| 'specialist': '#F59E0B', |
| 'generalist': '#EC4899', |
| 'tool_user': '#10B981', |
| 'monitor': '#6366F1' |
| } |
| return typeColors[type] || '#6B7280' |
| } |
| |
| |
| const validateAndCleanCapabilities = (capabilitiesString) => { |
| if (!capabilitiesString) return getDefaultCapabilities(form.type) |
| |
| const allowedCapabilities = new Set([ |
| 'orchestration', 'coordination', 'strategy', |
| 'coding', 'debugging', 'architecture', |
| 'analysis', 'research', 'reporting', |
| 'medical_advice', 'diagnosis', 'treatment', |
| 'legal_advice', 'compliance', 'contracts', |
| 'financial_analysis', 'investment', 'budgeting', |
| 'system_integration', 'devops', 'monitoring', |
| 'coaching', 'training', 'change_management' |
| ]) |
| |
| const userCapabilities = capabilitiesString |
| .split(',') |
| .map(cap => cap.trim().toLowerCase().replace(/\s+/g, '_')) |
| .filter(cap => cap && allowedCapabilities.has(cap)) |
| |
| return userCapabilities.length > 0 ? userCapabilities : getDefaultCapabilities(form.type) |
| } |
| |
| |
| const getDefaultCapabilities = (type) => { |
| const defaultCapabilities = { |
| 'coordinator': ['orchestration', 'coordination', 'strategy'], |
| 'developer': ['coding', 'debugging', 'architecture'], |
| 'specialist': ['research', 'analysis', 'strategy'], |
| 'generalist': ['analysis', 'research', 'reporting'], |
| 'tool_user': ['system_integration', 'devops', 'monitoring'], |
| 'monitor': ['monitoring', 'analysis', 'reporting'] |
| } |
| return defaultCapabilities[type] || ['analysis', 'research'] |
| } |
| |
| return { |
| form, |
| loading, |
| error, |
| successMessage, |
| isFormValid, |
| handleSubmit |
| } |
| } |
| } |
| </script> |
| |
| <style scoped> |
| .modal-overlay { |
| @apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50; |
| } |
| |
| .modal-content { |
| @apply bg-white rounded-lg shadow-xl max-w-md w-full mx-4 relative; |
| } |
| |
| .loading-overlay { |
| @apply absolute inset-0 bg-white bg-opacity-90 flex flex-col items-center justify-center z-10 rounded-lg; |
| } |
| |
| .loading-spinner { |
| @apply w-8 h-8 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin; |
| } |
| |
| .loading-text { |
| @apply mt-4 text-sm font-medium text-gray-600; |
| } |
| |
| .error-message { |
| @apply bg-red-50 border border-red-200 rounded-md p-3 mb-4 flex items-center space-x-2 text-red-700; |
| } |
| |
| .error-icon { |
| @apply text-lg; |
| } |
| |
| .success-message { |
| @apply bg-green-50 border border-green-200 rounded-md p-3 mb-4 flex items-center space-x-2 text-green-700; |
| } |
| |
| .success-icon { |
| @apply text-lg; |
| } |
| |
| .modal-header { |
| @apply flex items-center justify-between p-6 border-b border-gray-200; |
| } |
| |
| .modal-title { |
| @apply text-lg font-semibold text-gray-900; |
| } |
| |
| .close-button { |
| @apply text-gray-400 hover:text-gray-600 text-xl font-bold w-8 h-8 flex items-center justify-center; |
| } |
| |
| .modal-body { |
| @apply p-6; |
| } |
| |
| .form-group { |
| @apply mb-4; |
| } |
| |
| .form-label { |
| @apply block text-sm font-medium text-gray-700 mb-2; |
| } |
| |
| .form-input { |
| @apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed; |
| } |
| |
| .form-input:disabled { |
| @apply opacity-50; |
| } |
| |
| .form-help { |
| @apply block text-xs text-gray-500 mt-1; |
| } |
| |
| .form-actions { |
| @apply flex space-x-3 mt-6; |
| } |
| |
| .cancel-button { |
| @apply flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 font-medium disabled:opacity-50 disabled:cursor-not-allowed; |
| } |
| |
| .submit-button { |
| @apply flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium disabled:bg-gray-400 disabled:cursor-not-allowed; |
| } |
| </style> |
| |