fix(ai-config): editar cualquier config dejaba al agente sin cerebro

es_agente_bot no se podía marcar desde ninguna pantalla —el formulario nunca
mandaba el campo— y el update lo escribía igual con el valor cero. O sea que
guardar cualquier config desde /app/ai-config apagaba el cerebro del bot de
Telegram y del chat del panel, y no había forma de volver a prenderlo salvo
tocando la base.

- El update solo escribe los campos que vinieron en el body.
- El formulario tiene la casilla y el selector de bot de Telegram.
- Marcar una desmarca la anterior: GetAgenteBotAiConfig hace First(), así que
  con dos marcadas ganaba la que estuviera primero en la tabla.

Y el otro comportamiento raro: cuando ningún módulo coincidía, se usaba
"cualquier config activa". Eso podía elegir la de embeddings o la de Whisper,
que no conversan — el error que llegaba era del proveedor y no se parecía en
nada a la causa. Ahora esas quedan excluidas del comodín y, si no queda
ninguna usable, el error dice qué módulo asignar y dónde.

De paso: la etiqueta "IA / vCard" mentía (ese módulo alimenta además soporte,
el chat del panel y las plantillas), el comentario de is_active decía "solo uno
activo a la vez" cuando hace falta uno por módulo, y GetActiveAiConfig no la
usaba nadie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-17 21:16:15 -05:00
co-authored by Claude Opus 5
parent 6a3a8c9248
commit f5ffed7a13
4 changed files with 116 additions and 33 deletions
+34 -5
View File
@@ -200,6 +200,26 @@
<label for="ai_is_active" class="text-sm text-gray-700">Activo</label>
</div>
<div class="border border-gray-200 rounded-lg p-3 bg-gray-50">
<label class="flex items-center gap-2 cursor-pointer select-none">
<input x-model="form.es_agente_bot" type="checkbox" class="rounded text-[#8eb02f] focus:ring-[#8eb02f]">
<span class="text-sm text-gray-700">Es el cerebro del agente (bot de Telegram y chat del panel)</span>
</label>
<p class="text-[11px] text-gray-400 mt-1 ml-6">
Solo una config puede serlo: al marcar esta, se desmarca la anterior.
</p>
<div x-show="form.es_agente_bot" x-cloak class="mt-2 ml-6">
<label class="block text-xs font-medium text-gray-600 mb-1">Bot de Telegram (opcional)</label>
<select x-model="form.telegram_config_id"
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="">Sin bot asignado</option>
<template x-for="t in telegramConfigs" :key="t.ID">
<option :value="t.ID" x-text="t.nombre"></option>
</template>
</select>
</div>
</div>
<div>
<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">
@@ -288,18 +308,26 @@ function aiConfigApp() {
search: '',
showModal: false, editItem: null, deleteId: null, testResult: null,
errorMsg: '', successMsg: '', formError: '',
form: { nombre: '', provider: '', api_key: '', base_url: '', model_name: '', is_active: true, notes: '', modulos: [] },
form: { nombre: '', provider: '', api_key: '', base_url: '', model_name: '', is_active: true, notes: '', modulos: [], es_agente_bot: false, telegram_config_id: '' },
telegramConfigs: [],
moduleOptions: [
{ value: 'landing', label: 'Landing Generator' },
{ value: 'query_runner', label: 'Query Runner SQL' },
{ value: 'ia', label: 'IA / vCard' },
{ value: 'ia', label: 'IA general (vCard, soporte, chat del panel)' },
{ value: 'plantillas', label: 'Plantillas de documento (importar con IA)' },
{ value: 'whisper', label: 'Transcripción de audio (Whisper)' },
{ value: 'umind_embeddings', label: 'uMind — embeddings (RAG del widget)' },
],
async init() { await this.load() },
async init() {
try {
const r = await fetch('/app/loadtelegram')
const d = await r.json()
this.telegramConfigs = d.registros || d.items || d || []
} catch { this.telegramConfigs = [] }
await this.load()
},
async load() {
this.loading = true
@@ -326,7 +354,7 @@ function aiConfigApp() {
openAdd() {
this.editItem = null
this.form = { nombre: '', provider: 'qwen', api_key: '', base_url: '', model_name: 'qwen2.5-72b-instruct', is_active: true, notes: '', modulos: [] }
this.form = { nombre: '', provider: 'qwen', api_key: '', base_url: '', model_name: 'qwen2.5-72b-instruct', is_active: true, notes: '', modulos: [], es_agente_bot: false, telegram_config_id: '' }
this.formError = ''
this.showModal = true
},
@@ -336,7 +364,7 @@ function aiConfigApp() {
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.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, es_agente_bot: !!item.es_agente_bot, telegram_config_id: item.telegram_config_id || '' }
this.formError = ''
this.showModal = true
},
@@ -347,6 +375,7 @@ function aiConfigApp() {
this.saving = true; this.formError = ''
const payload = { ...this.form, modulo: this.form.modulos.join(',') }
delete payload.modulos
payload.telegram_config_id = payload.telegram_config_id ? parseInt(payload.telegram_config_id) : null
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(payload) })