diff --git a/pkg/models/ai_config.go b/pkg/models/ai_config.go index 6bc52af..3ebe9ed 100644 --- a/pkg/models/ai_config.go +++ b/pkg/models/ai_config.go @@ -1,7 +1,9 @@ package models import ( + "fmt" "log" + "strings" "github.com/sujit-baniya/fiber-boilerplate/app" "gorm.io/gorm" @@ -70,39 +72,62 @@ func GetActiveAiConfig(provider string) (*AiConfig, error) { return &item, nil } +// SplitModulos parte el campo Modulo (comma-separated) en un slice limpio. +// "" → [] (config global), "landing,query_runner" → ["landing","query_runner"] +func SplitModulos(modulo string) []string { + var out []string + for _, s := range strings.Split(modulo, ",") { + if s = strings.TrimSpace(s); s != "" { + out = append(out, s) + } + } + return out +} + +// JoinModulos normaliza y une un slice de módulos en comma-separated. +func JoinModulos(modules []string) string { + var clean []string + for _, s := range modules { + if s = strings.TrimSpace(s); s != "" { + clean = append(clean, s) + } + } + return strings.Join(clean, ",") +} + // GetAiConfigForService retorna la config activa asignada al módulo indicado. // Lógica de prioridad: -// 1. Config activa con modulo == service (exclusiva del servicio) +// 1. Config activa con modulo conteniendo service (puede ser comma-separated) // 2. Config activa con modulo == "" (global, fallback) // 3. Cualquier config activa (último recurso) func GetAiConfigForService(service string) (*AiConfig, error) { - var item AiConfig + var items []AiConfig + if err := app.Http.Database.DB.Where("is_active = ?", true).Order("id ASC").Find(&items).Error; err != nil { + return nil, fmt.Errorf("error leyendo ai_configs: %w", err) + } + if len(items) == 0 { + return nil, fmt.Errorf("no hay configs de IA activas") + } - // 1. Buscar config específica para el servicio + // 1. Config específica para el servicio (puede tener varios módulos) if service != "" { - err := app.Http.Database.DB. - Where("is_active = ? AND modulo = ?", true, service). - First(&item).Error - if err == nil { - return &item, nil + for i := range items { + for _, m := range SplitModulos(items[i].Modulo) { + if m == service { + return &items[i], nil + } + } } } - // 2. Buscar config global (modulo vacío) - err := app.Http.Database.DB. - Where("is_active = ? AND (modulo = '' OR modulo IS NULL)", true). - First(&item).Error - if err == nil { - return &item, nil + // 2. Config global (modulo vacío = sin restricción de servicio) + for i := range items { + if strings.TrimSpace(items[i].Modulo) == "" { + return &items[i], nil + } } // 3. Cualquier config activa como último recurso - err = app.Http.Database.DB. - Where("is_active = ?", true). - First(&item).Error - if err != nil { - log.Printf("[AI_CONFIG] No se encontró config activa para servicio '%s': %v", service, err) - return nil, err - } - return &item, nil + log.Printf("[AI_CONFIG] No se encontró config para servicio '%s', usando cualquier activa", service) + return &items[0], nil } diff --git a/resources/views/ai_config.html b/resources/views/ai_config.html index 47ca1ed..cdceaac 100644 --- a/resources/views/ai_config.html +++ b/resources/views/ai_config.html @@ -64,14 +64,22 @@ - - + +
- - + +
+ + +

- Asigna esta IA a un servicio específico. "Global" actúa de fallback cuando un servicio no tiene IA propia. + "Global" actúa de fallback. Si seleccionas servicios específicos, solo se usará para ellos.

@@ -231,7 +253,12 @@ function aiConfigApp() { search: '', showModal: false, editItem: null, deleteId: null, errorMsg: '', successMsg: '', formError: '', - form: { nombre: '', provider: '', api_key: '', base_url: '', model_name: '', is_active: true, notes: '', modulo: '' }, + form: { nombre: '', provider: '', api_key: '', base_url: '', model_name: '', is_active: true, notes: '', modulos: [] }, + + moduleOptions: [ + { value: 'landing', label: 'Landing Generator' }, + { value: 'query_runner', label: 'Query Runner SQL' }, + ], async init() { await this.load() }, @@ -249,16 +276,28 @@ function aiConfigApp() { goPage(p) { this.page = p; this.load() }, + toggleModulo(value) { + const idx = this.form.modulos.indexOf(value) + if (idx >= 0) { + this.form.modulos.splice(idx, 1) + } else { + this.form.modulos.push(value) + } + }, + openAdd() { this.editItem = null - this.form = { nombre: '', provider: 'qwen', api_key: '', base_url: '', model_name: 'qwen2.5-72b-instruct', is_active: true, notes: '', modulo: '' } + this.form = { nombre: '', provider: 'qwen', api_key: '', base_url: '', model_name: 'qwen2.5-72b-instruct', is_active: true, notes: '', modulos: [] } this.formError = '' this.showModal = true }, openEdit(item) { this.editItem = item - this.form = { nombre: item.nombre, provider: item.provider, api_key: '', base_url: item.base_url, model_name: item.model_name, is_active: item.is_active, notes: item.notes, modulo: item.modulo || '' } + const modulos = item.modulo + ? item.modulo.split(',').map(s => s.trim()).filter(s => s) + : [] + this.form = { nombre: item.nombre, provider: item.provider, api_key: '', base_url: item.base_url, model_name: item.model_name, is_active: item.is_active, notes: item.notes, modulos } this.formError = '' this.showModal = true }, @@ -267,9 +306,11 @@ function aiConfigApp() { async save() { this.saving = true; this.formError = '' + const payload = { ...this.form, modulo: this.form.modulos.join(',') } + delete payload.modulos const url = this.editItem ? `/app/ai-config/${this.editItem.ID}` : '/app/ai-config' const method = this.editItem ? 'PUT' : 'POST' - const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.form) }) + const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) const data = await res.json() this.saving = false if (!res.ok) { this.formError = data.error || 'Error guardando'; return } diff --git a/rest/controllers/ai_config_controller.go b/rest/controllers/ai_config_controller.go index bf8a394..3d23495 100644 --- a/rest/controllers/ai_config_controller.go +++ b/rest/controllers/ai_config_controller.go @@ -106,7 +106,7 @@ func CreateAiConfigHandler(c *fiber.Ctx) error { ModelName: strings.TrimSpace(req.ModelName), IsActive: req.IsActive, Notes: req.Notes, - Modulo: strings.TrimSpace(req.Modulo), + Modulo: models.JoinModulos(strings.Split(req.Modulo, ",")), } if err := models.CreateAiConfig(&item); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -143,7 +143,7 @@ func UpdateAiConfigHandler(c *fiber.Ctx) error { "model_name": strings.TrimSpace(req.ModelName), "is_active": req.IsActive, "notes": req.Notes, - "modulo": strings.TrimSpace(req.Modulo), + "modulo": models.JoinModulos(strings.Split(req.Modulo, ",")), } if strings.TrimSpace(req.ApiKey) != "" { updates["api_key"] = strings.TrimSpace(req.ApiKey)