saap-plattform / frontend /src /components /modals /AgentSettingsModal.vue
Hwandji's picture
feat: initial HuggingFace Space deployment
4343907
raw
history blame
18.5 kB
<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>