Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="modal-overlay" @click.self="$emit('close')"> | |
| <div class="modal-container"> | |
| <div class="modal-header"> | |
| <h3 class="modal-title">Agent Settings</h3> | |
| <button | |
| class="modal-close-btn" | |
| @click="$emit('close')" | |
| > | |
| <XIcon class="close-icon" /> | |
| </button> | |
| </div> | |
| <div class="modal-body"> | |
| <div v-if="loading" class="loading-state"> | |
| <div class="spinner"></div> | |
| <p>Loading agent settings...</p> | |
| </div> | |
| <form v-else @submit.prevent="saveSettings" class="settings-form"> | |
| <!-- Basic Information --> | |
| <div class="form-section"> | |
| <h4 class="section-title">Basic Information</h4> | |
| <div class="form-group"> | |
| <label class="form-label" for="agent-name">Agent Name *</label> | |
| <input | |
| id="agent-name" | |
| v-model="formData.name" | |
| type="text" | |
| class="form-input" | |
| :class="{ 'error': errors.name }" | |
| required | |
| maxlength="100" | |
| /> | |
| <div v-if="errors.name" class="form-error">{{ errors.name }}</div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="agent-description">Description</label> | |
| <textarea | |
| id="agent-description" | |
| v-model="formData.description" | |
| class="form-textarea" | |
| :class="{ 'error': errors.description }" | |
| rows="3" | |
| maxlength="500" | |
| placeholder="Describe the agent's role and capabilities..." | |
| ></textarea> | |
| <div class="form-help">{{ formData.description?.length || 0 }}/500 characters</div> | |
| <div v-if="errors.description" class="form-error">{{ errors.description }}</div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="agent-type">Agent Type</label> | |
| <select | |
| id="agent-type" | |
| v-model="formData.type" | |
| class="form-select" | |
| :class="{ 'error': errors.type }" | |
| > | |
| <option value="coordinator">Coordinator</option> | |
| <option value="developer">Developer</option> | |
| <option value="specialist">Medical Specialist</option> | |
| <option value="analyst">Analyst</option> | |
| <option value="support">Support</option> | |
| <option value="finance">Finance</option> | |
| <option value="system">System</option> | |
| </select> | |
| <div v-if="errors.type" class="form-error">{{ errors.type }}</div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="agent-color">Agent Color</label> | |
| <div class="color-input-group"> | |
| <input | |
| id="agent-color" | |
| v-model="formData.color" | |
| type="color" | |
| class="form-color-input" | |
| /> | |
| <input | |
| v-model="formData.color" | |
| type="text" | |
| class="form-input color-text-input" | |
| pattern="^#[0-9A-Fa-f]{6}$" | |
| placeholder="#6B7280" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Capabilities --> | |
| <div class="form-section"> | |
| <h4 class="section-title">Capabilities</h4> | |
| <div class="capabilities-editor"> | |
| <div class="capabilities-list"> | |
| <div | |
| v-for="(capability, index) in formData.capabilities" | |
| :key="index" | |
| class="capability-item" | |
| > | |
| <input | |
| v-model="formData.capabilities[index]" | |
| type="text" | |
| class="capability-input" | |
| placeholder="Enter capability..." | |
| @keydown.enter.prevent="addCapability" | |
| /> | |
| <button | |
| type="button" | |
| class="remove-capability-btn" | |
| @click="removeCapability(index)" | |
| > | |
| <MinusIcon class="btn-icon" /> | |
| </button> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| class="add-capability-btn" | |
| @click="addCapability" | |
| > | |
| <PlusIcon class="btn-icon" /> | |
| Add Capability | |
| </button> | |
| </div> | |
| </div> | |
| <!-- LLM Configuration --> | |
| <div class="form-section"> | |
| <h4 class="section-title">LLM Configuration</h4> | |
| <div class="form-group"> | |
| <label class="form-label" for="llm-provider">Provider</label> | |
| <select | |
| id="llm-provider" | |
| v-model="formData.llm_config.provider" | |
| class="form-select" | |
| > | |
| <option value="colossus">colossus (Primary)</option> | |
| <option value="openrouter">OpenRouter (Cost-efficient)</option> | |
| <option value="ollama">Ollama (Local)</option> | |
| <option value="openai">OpenAI</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="llm-model">Model</label> | |
| <input | |
| id="llm-model" | |
| v-model="formData.llm_config.model" | |
| type="text" | |
| class="form-input" | |
| placeholder="e.g., mistral-small3.2:24b-instruct-2506" | |
| /> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="max-tokens">Max Tokens</label> | |
| <input | |
| id="max-tokens" | |
| v-model.number="formData.llm_config.max_tokens" | |
| type="number" | |
| class="form-input" | |
| min="1" | |
| max="8192" | |
| placeholder="2048" | |
| /> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="temperature">Temperature</label> | |
| <div class="range-input-group"> | |
| <input | |
| id="temperature" | |
| v-model.number="formData.llm_config.temperature" | |
| type="range" | |
| class="form-range" | |
| min="0" | |
| max="2" | |
| step="0.1" | |
| /> | |
| <span class="range-value">{{ formData.llm_config.temperature }}</span> | |
| </div> | |
| <div class="form-help">Higher values make output more creative, lower values more focused</div> | |
| </div> | |
| </div> | |
| <!-- Communication Settings --> | |
| <div class="form-section"> | |
| <h4 class="section-title">Communication Settings</h4> | |
| <div class="form-group"> | |
| <label class="form-label"> | |
| <input | |
| v-model="formData.communication.auto_start" | |
| type="checkbox" | |
| class="form-checkbox" | |
| /> | |
| Auto-start agent on system boot | |
| </label> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="response-timeout">Response Timeout (seconds)</label> | |
| <input | |
| id="response-timeout" | |
| v-model.number="formData.communication.timeout" | |
| type="number" | |
| class="form-input" | |
| min="5" | |
| max="300" | |
| placeholder="30" | |
| /> | |
| </div> | |
| </div> | |
| </form> | |
| <!-- Error Display --> | |
| <div v-if="error" class="error-message"> | |
| <AlertTriangleIcon class="error-icon" /> | |
| <span>{{ error }}</span> | |
| </div> | |
| <!-- Success Display --> | |
| <div v-if="success" class="success-message"> | |
| <CheckCircleIcon class="success-icon" /> | |
| <span>Agent settings updated successfully!</span> | |
| </div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button | |
| type="button" | |
| class="btn-secondary" | |
| @click="$emit('close')" | |
| :disabled="saving" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="button" | |
| class="btn-primary" | |
| @click="saveSettings" | |
| :disabled="saving || !isFormValid" | |
| > | |
| <div v-if="saving" class="btn-spinner"></div> | |
| {{ saving ? 'Saving...' : 'Save Changes' }} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| import { ref, reactive, computed, onMounted, watch } from 'vue' | |
| import { | |
| XIcon, PlusIcon, MinusIcon, | |
| AlertTriangleIcon, CheckCircleIcon | |
| } from 'lucide-vue-next' | |
| import { saapApi } from '../../services/saapApi.js' | |
| export default { | |
| name: 'AgentSettingsModal', | |
| components: { | |
| XIcon, | |
| PlusIcon, | |
| MinusIcon, | |
| AlertTriangleIcon, | |
| CheckCircleIcon | |
| }, | |
| props: { | |
| agent: { | |
| type: Object, | |
| required: true | |
| } | |
| }, | |
| emits: ['close', 'agent-updated'], | |
| setup(props, { emit }) { | |
| const loading = ref(true) | |
| const saving = ref(false) | |
| const error = ref('') | |
| const success = ref(false) | |
| const errors = reactive({}) | |
| // Form data - deep copy of agent data | |
| const formData = reactive({ | |
| name: '', | |
| description: '', | |
| type: 'coordinator', | |
| color: '#6B7280', | |
| capabilities: [], | |
| llm_config: { | |
| provider: 'colossus', | |
| model: 'mistral-small3.2:24b-instruct-2506', | |
| max_tokens: 2048, | |
| temperature: 0.7 | |
| }, | |
| communication: { | |
| auto_start: false, | |
| timeout: 30 | |
| } | |
| }) | |
| // Initialize form data | |
| const initializeFormData = () => { | |
| if (!props.agent) return | |
| // Basic info | |
| formData.name = props.agent.name || '' | |
| formData.description = props.agent.description || '' | |
| formData.type = props.agent.type || 'coordinator' | |
| formData.color = props.agent.color || '#6B7280' | |
| // Capabilities | |
| if (Array.isArray(props.agent.capabilities)) { | |
| formData.capabilities = [...props.agent.capabilities] | |
| } else if (typeof props.agent.capabilities === 'string') { | |
| formData.capabilities = props.agent.capabilities.split(',').map(cap => cap.trim()).filter(Boolean) | |
| } else { | |
| formData.capabilities = [] | |
| } | |
| // LLM config | |
| if (props.agent.llm_config) { | |
| formData.llm_config = { | |
| provider: props.agent.llm_config.provider || 'colossus', | |
| model: props.agent.llm_config.model || 'mistral-small3.2:24b-instruct-2506', | |
| max_tokens: props.agent.llm_config.max_tokens || 2048, | |
| temperature: props.agent.llm_config.temperature || 0.7 | |
| } | |
| } | |
| // Communication | |
| if (props.agent.communication) { | |
| formData.communication = { | |
| auto_start: props.agent.communication.auto_start || false, | |
| timeout: props.agent.communication.timeout || 30 | |
| } | |
| } | |
| loading.value = false | |
| } | |
| // Form validation | |
| const isFormValid = computed(() => { | |
| return formData.name?.trim() && | |
| formData.type && | |
| Object.keys(errors).length === 0 | |
| }) | |
| // Validate form | |
| const validateForm = () => { | |
| // Clear previous errors | |
| Object.keys(errors).forEach(key => delete errors[key]) | |
| // Name validation | |
| if (!formData.name?.trim()) { | |
| errors.name = 'Agent name is required' | |
| } else if (formData.name.length > 100) { | |
| errors.name = 'Agent name must be less than 100 characters' | |
| } | |
| // Description validation | |
| if (formData.description && formData.description.length > 500) { | |
| errors.description = 'Description must be less than 500 characters' | |
| } | |
| return Object.keys(errors).length === 0 | |
| } | |
| // Capability management | |
| const addCapability = () => { | |
| formData.capabilities.push('') | |
| } | |
| const removeCapability = (index) => { | |
| formData.capabilities.splice(index, 1) | |
| } | |
| // Save settings | |
| const saveSettings = async () => { | |
| if (!validateForm()) { | |
| return | |
| } | |
| saving.value = true | |
| error.value = '' | |
| success.value = false | |
| try { | |
| // Prepare update data | |
| const updateData = { | |
| id: props.agent.id || props.agent.agent_id, | |
| name: formData.name, | |
| description: formData.description, | |
| type: formData.type, | |
| color: formData.color, | |
| capabilities: formData.capabilities.filter(cap => cap.trim()), | |
| llm_config: formData.llm_config, | |
| communication: formData.communication, | |
| // Preserve existing metadata | |
| metadata: { | |
| ...props.agent.metadata, | |
| updated: new Date().toISOString() | |
| } | |
| } | |
| console.log('🔄 Updating agent:', updateData) | |
| // Call API | |
| const response = await saapApi.put(`/agents/${props.agent.id || props.agent.agent_id}`, updateData) | |
| console.log('✅ Agent updated:', response.data) | |
| // Show success | |
| success.value = true | |
| // Emit update event | |
| emit('agent-updated', response.data.agent || response.data) | |
| // Close modal after short delay | |
| setTimeout(() => { | |
| emit('close') | |
| }, 1000) | |
| } catch (err) { | |
| console.error('❌ Failed to update agent:', err) | |
| if (err.response?.data?.detail) { | |
| error.value = err.response.data.detail | |
| } else if (err.response?.status === 404) { | |
| error.value = 'Agent not found' | |
| } else if (err.response?.status === 400) { | |
| error.value = 'Invalid agent configuration' | |
| } else { | |
| error.value = 'Failed to update agent settings. Please try again.' | |
| } | |
| } finally { | |
| saving.value = false | |
| } | |
| } | |
| // Watch for agent changes | |
| watch(() => props.agent, () => { | |
| if (props.agent) { | |
| initializeFormData() | |
| } | |
| }, { immediate: true }) | |
| onMounted(() => { | |
| initializeFormData() | |
| }) | |
| return { | |
| loading, | |
| saving, | |
| error, | |
| success, | |
| errors, | |
| formData, | |
| isFormValid, | |
| addCapability, | |
| removeCapability, | |
| saveSettings | |
| } | |
| } | |
| } | |
| </script> | |
| <style scoped> | |
| .modal-overlay { | |
| @apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4; | |
| } | |
| .modal-container { | |
| @apply bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-screen overflow-hidden; | |
| } | |
| .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; | |
| } | |
| .modal-close-btn { | |
| @apply p-2 text-gray-400 hover:text-gray-600 transition-colors duration-200 rounded-lg; | |
| } | |
| .close-icon { | |
| @apply w-5 h-5; | |
| } | |
| .modal-body { | |
| @apply p-6 max-h-96 overflow-y-auto; | |
| } | |
| .loading-state { | |
| @apply flex flex-col items-center justify-center py-12 text-gray-500; | |
| } | |
| .spinner { | |
| @apply w-8 h-8 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin mb-4; | |
| } | |
| .settings-form { | |
| @apply space-y-8; | |
| } | |
| .form-section { | |
| @apply space-y-4; | |
| } | |
| .section-title { | |
| @apply text-base font-semibold text-gray-900 border-b border-gray-100 pb-2; | |
| } | |
| .form-group { | |
| @apply space-y-2; | |
| } | |
| .form-label { | |
| @apply block text-sm font-medium text-gray-700; | |
| } | |
| .form-input { | |
| @apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors; | |
| } | |
| .form-input.error { | |
| @apply border-red-300 focus:ring-red-500; | |
| } | |
| .form-textarea { | |
| @apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors resize-y; | |
| } | |
| .form-select { | |
| @apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent; | |
| } | |
| .form-checkbox { | |
| @apply mr-2 text-blue-600 border-gray-300 rounded focus:ring-blue-500; | |
| } | |
| .form-range { | |
| @apply w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer; | |
| } | |
| .color-input-group { | |
| @apply flex space-x-2; | |
| } | |
| .form-color-input { | |
| @apply w-12 h-10 border border-gray-300 rounded-lg cursor-pointer; | |
| } | |
| .color-text-input { | |
| @apply flex-1; | |
| } | |
| .range-input-group { | |
| @apply flex items-center space-x-3; | |
| } | |
| .range-value { | |
| @apply text-sm font-medium text-gray-700 min-w-8; | |
| } | |
| .form-help { | |
| @apply text-xs text-gray-500; | |
| } | |
| .form-error { | |
| @apply text-sm text-red-600; | |
| } | |
| .capabilities-editor { | |
| @apply space-y-3; | |
| } | |
| .capabilities-list { | |
| @apply space-y-2; | |
| } | |
| .capability-item { | |
| @apply flex items-center space-x-2; | |
| } | |
| .capability-input { | |
| @apply flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent; | |
| } | |
| .remove-capability-btn { | |
| @apply p-2 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors; | |
| } | |
| .add-capability-btn { | |
| @apply inline-flex items-center px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors; | |
| } | |
| .btn-icon { | |
| @apply w-4 h-4 mr-1; | |
| } | |
| .error-message { | |
| @apply flex items-center space-x-2 p-4 bg-red-50 text-red-700 rounded-lg; | |
| } | |
| .success-message { | |
| @apply flex items-center space-x-2 p-4 bg-green-50 text-green-700 rounded-lg; | |
| } | |
| .error-icon, | |
| .success-icon { | |
| @apply w-5 h-5; | |
| } | |
| .modal-footer { | |
| @apply flex items-center justify-end space-x-3 p-6 border-t border-gray-200; | |
| } | |
| .btn-secondary { | |
| @apply px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors; | |
| } | |
| .btn-primary { | |
| @apply px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-colors flex items-center; | |
| } | |
| .btn-spinner { | |
| @apply w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 640px) { | |
| .modal-container { | |
| @apply mx-4 max-h-screen; | |
| } | |
| .modal-body { | |
| @apply px-4 max-h-80; | |
| } | |
| .form-section { | |
| @apply space-y-3; | |
| } | |
| } | |
| </style> |