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>
416 lines
25 KiB
HTML
416 lines
25 KiB
HTML
<!-- Vista: Configuraciones de IA — Qwen, OpenAI, etc. -->
|
|
<div x-data="aiConfigApp()" x-init="init()" @keydown.escape.window="closeModal()" class="bg-white rounded-lg shadow">
|
|
|
|
<div x-show="loading" class="fixed inset-0 bg-gray-800 bg-opacity-75 flex justify-center items-center z-50">
|
|
<img src="../img/loading.gif" alt="Cargando..." class="w-16 h-16" />
|
|
</div>
|
|
|
|
<div class="container mx-auto p-6 w-full">
|
|
|
|
<!-- Header -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold">Configuraciones de IA</h1>
|
|
<p class="text-xs text-slate-500 mt-0.5">Gestiona las claves de API para Qwen, OpenAI y otros proveedores de IA.</p>
|
|
</div>
|
|
<button @click="openAdd()"
|
|
class="flex items-center gap-2 text-white text-sm font-medium px-4 py-2 rounded-lg"
|
|
style="background-color:#8eb02f"
|
|
onmouseover="this.style.backgroundColor='#6d8c24'"
|
|
onmouseout="this.style.backgroundColor='#8eb02f'">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
|
|
</svg>
|
|
Nueva configuración
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Alerta -->
|
|
<div x-show="errorMsg" x-cloak class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700" x-text="errorMsg"></div>
|
|
<div x-show="successMsg" x-cloak class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-sm text-green-700" x-text="successMsg"></div>
|
|
|
|
<!-- Búsqueda -->
|
|
<div class="flex flex-col sm:flex-row gap-3 mb-5">
|
|
<input x-model="search" @keyup.enter="load()" type="text" placeholder="Buscar por nombre o provider..."
|
|
class="border border-gray-300 rounded-lg px-3 py-2 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
|
|
<button @click="load()" :disabled="loading"
|
|
class="flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-300 text-sm hover:bg-gray-50 transition disabled:opacity-40">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" :class="loading && 'animate-spin'" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"/>
|
|
</svg>
|
|
<span x-text="loading ? 'Cargando...' : 'Buscar'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tabla -->
|
|
<div class="overflow-x-auto">
|
|
<table class="table-auto w-full text-sm">
|
|
<thead class="border-b border-gray-200 text-left text-xs font-semibold text-gray-500 uppercase">
|
|
<tr>
|
|
<th class="py-2 px-3">Nombre</th>
|
|
<th class="py-2 px-3">Módulo</th>
|
|
<th class="py-2 px-3">Provider</th>
|
|
<th class="py-2 px-3">Modelo</th>
|
|
<th class="py-2 px-3">API Key</th>
|
|
<th class="py-2 px-3">Estado</th>
|
|
<th class="py-2 px-3 text-right">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
<template x-if="items.length === 0">
|
|
<tr><td colspan="7" class="py-8 text-center text-gray-400">Sin configuraciones</td></tr>
|
|
</template>
|
|
<template x-for="item in items" :key="item.ID">
|
|
<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">
|
|
<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',
|
|
'bg-purple-100 text-purple-700': m.trim() === 'ia',
|
|
'bg-pink-100 text-pink-700': m.trim() === 'whisper',
|
|
'bg-indigo-100 text-indigo-700': m.trim() === 'umind_embeddings'
|
|
}"
|
|
x-text="m.trim() === 'landing' ? 'Landing' : m.trim() === 'query_runner' ? 'Query Runner' : m.trim() === 'ia' ? 'IA / vCard' : m.trim() === 'whisper' ? 'Whisper' : m.trim() === 'umind_embeddings' ? 'uMind (embeddings)' : 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"
|
|
:class="{
|
|
'bg-purple-100 text-purple-700': item.provider === 'qwen',
|
|
'bg-green-100 text-green-700': item.provider === 'openai',
|
|
'bg-orange-100 text-orange-700': item.provider === 'anthropic',
|
|
'bg-blue-100 text-blue-700': item.provider === 'ollama',
|
|
'bg-yellow-100 text-yellow-700': item.provider === 'gemini',
|
|
'bg-gray-100 text-gray-700': !['qwen','openai','anthropic','ollama','gemini'].includes(item.provider)
|
|
}" x-text="item.provider"></span>
|
|
</td>
|
|
<td class="py-2 px-3 text-gray-500 font-mono text-xs" x-text="item.model_name || '—'"></td>
|
|
<td class="py-2 px-3 font-mono text-xs text-gray-400" x-text="item.api_key_hint"></td>
|
|
<td class="py-2 px-3">
|
|
<span :class="item.is_active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'"
|
|
class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
|
x-text="item.is_active ? 'Activo' : 'Inactivo'"></span>
|
|
</td>
|
|
<td class="py-2 px-3 text-right">
|
|
<div class="flex justify-end gap-2">
|
|
<button @click="testConfig(item.ID)"
|
|
class="text-xs text-green-600 hover:text-green-800 font-medium transition">Probar</button>
|
|
<button @click="openEdit(item)"
|
|
class="text-xs text-blue-600 hover:text-blue-800 font-medium transition">Editar</button>
|
|
<button @click="confirmDelete(item.ID)"
|
|
class="text-xs text-red-500 hover:text-red-700 font-medium transition">Eliminar</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Paginación -->
|
|
<div x-show="totalPages > 1" class="flex justify-center gap-1 mt-5">
|
|
<template x-for="p in totalPages" :key="p">
|
|
<button @click="goPage(p)" :class="p === page ? 'bg-[#8eb02f] text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'"
|
|
class="w-8 h-8 rounded text-sm font-medium transition" x-text="p"></button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal crear / editar -->
|
|
<div x-show="showModal" x-cloak class="fixed inset-0 bg-black/50 flex items-center justify-center z-40 p-4">
|
|
<div @click.outside="closeModal()" class="bg-white rounded-xl shadow-xl w-full max-w-lg p-6">
|
|
<h2 class="text-lg font-bold mb-4" x-text="editItem ? 'Editar configuración' : 'Nueva configuración de IA'"></h2>
|
|
<form @submit.prevent="save()">
|
|
<div class="grid grid-cols-1 gap-4">
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Nombre *</label>
|
|
<input x-model="form.nombre" type="text" required placeholder="Ej: Qwen 2.5 Producción"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Provider *</label>
|
|
<select x-model="form.provider" required
|
|
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="">Seleccionar...</option>
|
|
<option value="qwen">Qwen (Alibaba)</option>
|
|
<option value="openai">OpenAI</option>
|
|
<option value="anthropic">Anthropic</option>
|
|
<option value="groq">Groq</option>
|
|
<option value="ollama">Ollama (local)</option>
|
|
<option value="gemini">Google Gemini</option>
|
|
<option value="otro">Otro</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Modelo</label>
|
|
<input x-model="form.model_name" type="text" placeholder="Ej: gemini-2.0-flash"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">
|
|
API Key <span x-show="editItem" class="text-gray-400">(dejar vacío para no cambiar)</span> <span x-show="!editItem">*</span>
|
|
</label>
|
|
<input x-model="form.api_key" type="password" autocomplete="new-password"
|
|
:required="!editItem"
|
|
placeholder="sk-..."
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f] font-mono" />
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Base URL
|
|
<span x-show="form.provider !== 'ollama' && form.provider !== 'gemini'" class="text-gray-400">(opcional — dejar vacío para usar el default del provider)</span>
|
|
<span x-show="form.provider === 'ollama'" class="text-blue-500">* requerido — incluir /v1 al final</span>
|
|
<span x-show="form.provider === 'gemini'" class="text-gray-400">(fijo — no necesita)</span>
|
|
</label>
|
|
<input x-model="form.base_url" type="url"
|
|
:placeholder="form.provider === 'ollama' ? 'http://10.0.1.15:11434/v1' : form.provider === 'gemini' ? 'https://generativelanguage.googleapis.com' : 'https://dashscope.aliyuncs.com/compatible-mode/v1'"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
|
|
<p x-show="form.provider === 'ollama'" x-cloak class="text-[10px] text-blue-500 mt-1">
|
|
Interna (red Coolify): <code>http://10.0.1.15:11434/v1</code> — Pública: <code>https://ollama.u-s.app/v1</code>
|
|
</p>
|
|
<p x-show="form.provider === 'gemini'" x-cloak class="text-[10px] text-yellow-600 mt-1">
|
|
Usa <code>https://generativelanguage.googleapis.com/v1beta/models/{model}:streamGenerateContent?alt=sse</code>. Solo necesitas el API Key.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Notas</label>
|
|
<textarea x-model="form.notes" rows="2" placeholder="Uso, límites, etc."
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"></textarea>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<input x-model="form.is_active" type="checkbox" id="ai_is_active" class="rounded" />
|
|
<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">
|
|
<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">
|
|
"Global" actúa de fallback. Si seleccionas servicios específicos, solo se usará para ellos.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div x-show="formError" class="mt-3 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-600" x-text="formError"></div>
|
|
|
|
<div class="flex justify-end gap-3 mt-5">
|
|
<button type="button" @click="closeModal()"
|
|
class="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition">Cancelar</button>
|
|
<button type="submit" :disabled="saving"
|
|
class="px-4 py-2 text-sm text-white rounded-lg transition disabled:opacity-50"
|
|
style="background-color:#8eb02f"
|
|
onmouseover="this.style.backgroundColor='#6d8c24'"
|
|
onmouseout="this.style.backgroundColor='#8eb02f'">
|
|
<span x-text="saving ? 'Guardando...' : (editItem ? 'Actualizar' : 'Crear')"></span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal resultado test -->
|
|
<div x-show="testResult" x-cloak class="fixed inset-0 bg-black/50 flex items-center justify-center z-40 p-4">
|
|
<div @click.outside="testResult = null" class="bg-white rounded-xl shadow-xl w-full max-w-md p-6">
|
|
<div class="flex items-center gap-3 mb-3">
|
|
<span x-show="testResult && testResult.ok" class="text-2xl">✅</span>
|
|
<span x-show="testResult && !testResult.ok" class="text-2xl">❌</span>
|
|
<h2 class="text-lg font-bold" x-text="testResult && testResult.ok ? 'Conexión exitosa' : 'Error de conexión'"></h2>
|
|
</div>
|
|
<pre x-show="testResult" class="text-xs bg-gray-50 border border-gray-200 rounded-lg p-3 overflow-x-auto max-h-60"
|
|
x-text="JSON.stringify(testResult, null, 2)"></pre>
|
|
<div class="flex justify-end mt-4">
|
|
<button @click="testResult = null" class="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">Cerrar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal confirmar eliminación -->
|
|
<div x-show="deleteId" x-cloak class="fixed inset-0 bg-black/50 flex items-center justify-center z-40 p-4">
|
|
<div class="bg-white rounded-xl shadow-xl w-full max-w-sm p-6 text-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-red-500 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
|
</svg>
|
|
<p class="text-gray-700 font-semibold mb-1">¿Eliminar configuración?</p>
|
|
<p class="text-xs text-gray-500 mb-5">Esta acción no se puede deshacer.</p>
|
|
<div class="flex justify-center gap-3">
|
|
<button @click="deleteId = null" class="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">Cancelar</button>
|
|
<button @click="doDelete()" :disabled="saving"
|
|
class="px-4 py-2 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 transition disabled:opacity-50">
|
|
<span x-text="saving ? 'Eliminando...' : 'Eliminar'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function aiConfigApp() {
|
|
return {
|
|
loading: false, saving: false,
|
|
items: [], total: 0, totalPages: 1, page: 1,
|
|
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: [], 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 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() {
|
|
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
|
|
this.errorMsg = ''
|
|
const res = await fetch(`/app/ai-config/list?page=${this.page}&search=${encodeURIComponent(this.search)}`)
|
|
const data = await res.json()
|
|
this.loading = false
|
|
if (!res.ok) { this.errorMsg = data.error || 'Error cargando datos'; return }
|
|
this.items = data.items || []
|
|
this.total = data.total
|
|
this.totalPages = data.totalPages
|
|
},
|
|
|
|
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: '', modulos: [], es_agente_bot: false, telegram_config_id: '' }
|
|
this.formError = ''
|
|
this.showModal = true
|
|
},
|
|
|
|
openEdit(item) {
|
|
this.editItem = item
|
|
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, es_agente_bot: !!item.es_agente_bot, telegram_config_id: item.telegram_config_id || '' }
|
|
this.formError = ''
|
|
this.showModal = true
|
|
},
|
|
|
|
closeModal() { this.showModal = false; this.editItem = null; this.formError = '' },
|
|
|
|
async save() {
|
|
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) })
|
|
const data = await res.json()
|
|
this.saving = false
|
|
if (!res.ok) { this.formError = data.error || 'Error guardando'; return }
|
|
this.closeModal()
|
|
this.showSuccess(this.editItem ? 'Configuración actualizada' : 'Configuración creada')
|
|
await this.load()
|
|
},
|
|
|
|
confirmDelete(id) { this.deleteId = id },
|
|
|
|
async doDelete() {
|
|
this.saving = true
|
|
const res = await fetch(`/app/ai-config/${this.deleteId}`, { method: 'DELETE' })
|
|
this.saving = false
|
|
this.deleteId = null
|
|
if (!res.ok) { this.errorMsg = 'Error eliminando'; return }
|
|
this.showSuccess('Configuración eliminada')
|
|
await this.load()
|
|
},
|
|
|
|
async testConfig(id) {
|
|
this.loading = true
|
|
const res = await fetch(`/app/ai-config/${id}/test`)
|
|
this.loading = false
|
|
this.testResult = await res.json()
|
|
},
|
|
|
|
showSuccess(msg) {
|
|
this.successMsg = msg
|
|
setTimeout(() => { this.successMsg = '' }, 3000)
|
|
}
|
|
}
|
|
}
|
|
</script>
|