| <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"> |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <div v-if="error" class="error-message"> |
| <AlertTriangleIcon class="error-icon" /> |
| <span>{{ error }}</span> |
| </div> |
| |
| |
| <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({}) |
| |
| |
| 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 |
| } |
| }) |
| |
| |
| const initializeFormData = () => { |
| if (!props.agent) return |
| |
| |
| formData.name = props.agent.name || '' |
| formData.description = props.agent.description || '' |
| formData.type = props.agent.type || 'coordinator' |
| formData.color = props.agent.color || '#6B7280' |
| |
| |
| 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 = [] |
| } |
| |
| |
| 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 |
| } |
| } |
| |
| |
| if (props.agent.communication) { |
| formData.communication = { |
| auto_start: props.agent.communication.auto_start || false, |
| timeout: props.agent.communication.timeout || 30 |
| } |
| } |
| |
| loading.value = false |
| } |
| |
| |
| const isFormValid = computed(() => { |
| return formData.name?.trim() && |
| formData.type && |
| Object.keys(errors).length === 0 |
| }) |
| |
| |
| const validateForm = () => { |
| |
| Object.keys(errors).forEach(key => delete errors[key]) |
| |
| |
| 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' |
| } |
| |
| |
| if (formData.description && formData.description.length > 500) { |
| errors.description = 'Description must be less than 500 characters' |
| } |
| |
| return Object.keys(errors).length === 0 |
| } |
| |
| |
| const addCapability = () => { |
| formData.capabilities.push('') |
| } |
| |
| const removeCapability = (index) => { |
| formData.capabilities.splice(index, 1) |
| } |
| |
| |
| const saveSettings = async () => { |
| if (!validateForm()) { |
| return |
| } |
| |
| saving.value = true |
| error.value = '' |
| success.value = false |
| |
| try { |
| |
| 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, |
| |
| metadata: { |
| ...props.agent.metadata, |
| updated: new Date().toISOString() |
| } |
| } |
| |
| console.log('🔄 Updating agent:', updateData) |
| |
| |
| const response = await saapApi.put(`/agents/${props.agent.id || props.agent.agent_id}`, updateData) |
| |
| console.log('✅ Agent updated:', response.data) |
| |
| |
| success.value = true |
| |
| |
| emit('agent-updated', response.data.agent || response.data) |
| |
| |
| 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(() => 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; |
| } |
| |
| |
| @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> |