up
This commit is contained in:
+47
-22
@@ -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
|
||||
}
|
||||
|
||||
@@ -64,14 +64,22 @@
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="py-2 px-3 font-medium" x-text="item.nombre"></td>
|
||||
<td class="py-2 px-3">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
||||
:class="{
|
||||
'bg-blue-100 text-blue-700': item.modulo === 'landing',
|
||||
'bg-[#e9f0cf] text-[#5a7a1e]': item.modulo === 'query_runner',
|
||||
'bg-gray-100 text-gray-500': !item.modulo
|
||||
}"
|
||||
x-text="item.modulo === 'landing' ? 'Landing' : item.modulo === 'query_runner' ? 'Query Runner' : 'Global'">
|
||||
</span>
|
||||
<template x-if="!item.modulo">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold bg-gray-100 text-gray-500">Global</span>
|
||||
</template>
|
||||
<template x-if="item.modulo">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template x-for="m in item.modulo.split(',').filter(x => x.trim())" :key="m">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
||||
:class="{
|
||||
'bg-blue-100 text-blue-700': m.trim() === 'landing',
|
||||
'bg-[#e9f0cf] text-[#5a7a1e]': m.trim() === 'query_runner'
|
||||
}"
|
||||
x-text="m.trim() === 'landing' ? 'Landing' : m.trim() === 'query_runner' ? 'Query Runner' : m.trim()">
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</td>
|
||||
<td class="py-2 px-3">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
||||
@@ -174,15 +182,29 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Módulo / Servicio</label>
|
||||
<select x-model="form.modulo"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
|
||||
<option value="">Global (usado por todos como fallback)</option>
|
||||
<option value="landing">Landing Generator</option>
|
||||
<option value="query_runner">Query Runner SQL</option>
|
||||
</select>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-2">Módulo / Servicio</label>
|
||||
<div class="border border-gray-200 rounded-lg p-3 space-y-2 bg-gray-50">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox"
|
||||
:checked="form.modulos.length === 0"
|
||||
@change="form.modulos = []"
|
||||
class="rounded text-[#8eb02f] focus:ring-[#8eb02f]">
|
||||
<span class="text-sm text-gray-700">Global</span>
|
||||
<span class="text-[10px] text-gray-400 ml-1">— fallback para todos los servicios</span>
|
||||
</label>
|
||||
<template x-for="opt in moduleOptions" :key="opt.value">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox"
|
||||
:value="opt.value"
|
||||
:checked="form.modulos.includes(opt.value)"
|
||||
@change="toggleModulo(opt.value)"
|
||||
class="rounded text-[#8eb02f] focus:ring-[#8eb02f]">
|
||||
<span class="text-sm text-gray-700" x-text="opt.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-[10px] text-gray-400 mt-1">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user