up
This commit is contained in:
+47
-22
@@ -1,7 +1,9 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -70,39 +72,62 @@ func GetActiveAiConfig(provider string) (*AiConfig, error) {
|
|||||||
return &item, nil
|
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.
|
// GetAiConfigForService retorna la config activa asignada al módulo indicado.
|
||||||
// Lógica de prioridad:
|
// 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)
|
// 2. Config activa con modulo == "" (global, fallback)
|
||||||
// 3. Cualquier config activa (último recurso)
|
// 3. Cualquier config activa (último recurso)
|
||||||
func GetAiConfigForService(service string) (*AiConfig, error) {
|
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 != "" {
|
if service != "" {
|
||||||
err := app.Http.Database.DB.
|
for i := range items {
|
||||||
Where("is_active = ? AND modulo = ?", true, service).
|
for _, m := range SplitModulos(items[i].Modulo) {
|
||||||
First(&item).Error
|
if m == service {
|
||||||
if err == nil {
|
return &items[i], nil
|
||||||
return &item, nil
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Buscar config global (modulo vacío)
|
// 2. Config global (modulo vacío = sin restricción de servicio)
|
||||||
err := app.Http.Database.DB.
|
for i := range items {
|
||||||
Where("is_active = ? AND (modulo = '' OR modulo IS NULL)", true).
|
if strings.TrimSpace(items[i].Modulo) == "" {
|
||||||
First(&item).Error
|
return &items[i], nil
|
||||||
if err == nil {
|
}
|
||||||
return &item, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Cualquier config activa como último recurso
|
// 3. Cualquier config activa como último recurso
|
||||||
err = app.Http.Database.DB.
|
log.Printf("[AI_CONFIG] No se encontró config para servicio '%s', usando cualquier activa", service)
|
||||||
Where("is_active = ?", true).
|
return &items[0], nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,14 +64,22 @@
|
|||||||
<tr class="hover:bg-gray-50 transition">
|
<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 font-medium" x-text="item.nombre"></td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
<template x-if="!item.modulo">
|
||||||
:class="{
|
<span class="px-2 py-0.5 rounded-full text-xs font-semibold bg-gray-100 text-gray-500">Global</span>
|
||||||
'bg-blue-100 text-blue-700': item.modulo === 'landing',
|
</template>
|
||||||
'bg-[#e9f0cf] text-[#5a7a1e]': item.modulo === 'query_runner',
|
<template x-if="item.modulo">
|
||||||
'bg-gray-100 text-gray-500': !item.modulo
|
<div class="flex flex-wrap gap-1">
|
||||||
}"
|
<template x-for="m in item.modulo.split(',').filter(x => x.trim())" :key="m">
|
||||||
x-text="item.modulo === 'landing' ? 'Landing' : item.modulo === 'query_runner' ? 'Query Runner' : 'Global'">
|
<span class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
||||||
</span>
|
: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>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
<span class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
||||||
@@ -174,15 +182,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-600 mb-1">Módulo / Servicio</label>
|
<label class="block text-xs font-medium text-gray-600 mb-2">Módulo / Servicio</label>
|
||||||
<select x-model="form.modulo"
|
<div class="border border-gray-200 rounded-lg p-3 space-y-2 bg-gray-50">
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
|
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||||
<option value="">Global (usado por todos como fallback)</option>
|
<input type="checkbox"
|
||||||
<option value="landing">Landing Generator</option>
|
:checked="form.modulos.length === 0"
|
||||||
<option value="query_runner">Query Runner SQL</option>
|
@change="form.modulos = []"
|
||||||
</select>
|
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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,7 +253,12 @@ function aiConfigApp() {
|
|||||||
search: '',
|
search: '',
|
||||||
showModal: false, editItem: null, deleteId: null,
|
showModal: false, editItem: null, deleteId: null,
|
||||||
errorMsg: '', successMsg: '', formError: '',
|
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() },
|
async init() { await this.load() },
|
||||||
|
|
||||||
@@ -249,16 +276,28 @@ function aiConfigApp() {
|
|||||||
|
|
||||||
goPage(p) { this.page = p; this.load() },
|
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() {
|
openAdd() {
|
||||||
this.editItem = null
|
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.formError = ''
|
||||||
this.showModal = true
|
this.showModal = true
|
||||||
},
|
},
|
||||||
|
|
||||||
openEdit(item) {
|
openEdit(item) {
|
||||||
this.editItem = 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.formError = ''
|
||||||
this.showModal = true
|
this.showModal = true
|
||||||
},
|
},
|
||||||
@@ -267,9 +306,11 @@ function aiConfigApp() {
|
|||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
this.saving = true; this.formError = ''
|
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 url = this.editItem ? `/app/ai-config/${this.editItem.ID}` : '/app/ai-config'
|
||||||
const method = this.editItem ? 'PUT' : 'POST'
|
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()
|
const data = await res.json()
|
||||||
this.saving = false
|
this.saving = false
|
||||||
if (!res.ok) { this.formError = data.error || 'Error guardando'; return }
|
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),
|
ModelName: strings.TrimSpace(req.ModelName),
|
||||||
IsActive: req.IsActive,
|
IsActive: req.IsActive,
|
||||||
Notes: req.Notes,
|
Notes: req.Notes,
|
||||||
Modulo: strings.TrimSpace(req.Modulo),
|
Modulo: models.JoinModulos(strings.Split(req.Modulo, ",")),
|
||||||
}
|
}
|
||||||
if err := models.CreateAiConfig(&item); err != nil {
|
if err := models.CreateAiConfig(&item); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
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),
|
"model_name": strings.TrimSpace(req.ModelName),
|
||||||
"is_active": req.IsActive,
|
"is_active": req.IsActive,
|
||||||
"notes": req.Notes,
|
"notes": req.Notes,
|
||||||
"modulo": strings.TrimSpace(req.Modulo),
|
"modulo": models.JoinModulos(strings.Split(req.Modulo, ",")),
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.ApiKey) != "" {
|
if strings.TrimSpace(req.ApiKey) != "" {
|
||||||
updates["api_key"] = strings.TrimSpace(req.ApiKey)
|
updates["api_key"] = strings.TrimSpace(req.ApiKey)
|
||||||
|
|||||||
Reference in New Issue
Block a user