feat: IA configurable — probar conexión, activar/inactivar

- Toggle activo/inactivo en configuración → guarda ia_activa en lab_config
- Botón "Probar" llama api/lab/test_gemini.php con la clave actual
- test_gemini.php envía pregunta mínima a Gemini y confirma respuesta
- Dashboard oculta botón y panel IA cuando ia_activa != '1'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-23 23:16:06 -05:00
co-authored by Claude Sonnet 4.6
parent 3b5c26214c
commit cd3d3bb9c0
4 changed files with 92 additions and 2 deletions
+1
View File
@@ -17,6 +17,7 @@ $permitidas = [
'empresa_telefono', 'empresa_email', 'empresa_ciudad', 'empresa_telefono', 'empresa_email', 'empresa_ciudad',
'doc_color', 'doc_logo_base64', 'doc_pie_pagina', 'doc_color', 'doc_logo_base64', 'doc_pie_pagina',
'gemini_api_key', 'gemini_api_key',
'ia_activa',
]; ];
$guardadas = 0; $guardadas = 0;
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* POST /api/lab/test_gemini.php
* Prueba la clave de Gemini con una pregunta mínima.
* Body JSON: { key: string }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireAdmin();
$datos = inputJson();
$key = trim($datos['key'] ?? '');
if (!$key) jsonError('Token vacío.');
$payload = json_encode([
'contents' => [['role' => 'user', 'parts' => [['text' => 'Responde solo con la palabra: OK']]]],
'generationConfig' => ['maxOutputTokens' => 10, 'temperature' => 0],
], JSON_UNESCAPED_UNICODE);
$ch = curl_init("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$key}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!$raw) jsonError('Sin respuesta del servidor de Google.', 502);
$gemini = json_decode($raw, true);
if ($code !== 200) {
jsonError($gemini['error']['message'] ?? "Error HTTP {$code}", 502);
}
jsonOk(['modelo' => 'gemini-2.0-flash', 'respuesta' => $gemini['candidates'][0]['content']['parts'][0]['text'] ?? '']);
+43 -2
View File
@@ -167,7 +167,14 @@ require_once __DIR__ . '/shared/components/sidebar.php';
<!-- ── Inteligencia Artificial ────────────────────────── --> <!-- ── Inteligencia Artificial ────────────────────────── -->
<div class="section-card"> <div class="section-card">
<h6><i class="fas fa-robot me-1"></i>Inteligencia Artificial — Google Gemini</h6> <div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-0"><i class="fas fa-robot me-1"></i>Inteligencia Artificial — Google Gemini</h6>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="c-ia-activa" role="switch"
<?= !empty($cfg['ia_activa']) && $cfg['ia_activa'] === '1' ? 'checked' : '' ?>>
<label class="form-check-label small fw-semibold" for="c-ia-activa">Activo</label>
</div>
</div>
<p class="text-muted small mb-3"> <p class="text-muted small mb-3">
El asistente IA del turnero usa Google Gemini Flash para responder preguntas El asistente IA del turnero usa Google Gemini Flash para responder preguntas
sobre el estado del día. Genera tu clave en sobre el estado del día. Genera tu clave en
@@ -184,8 +191,11 @@ require_once __DIR__ . '/shared/components/sidebar.php';
onclick="this.previousElementSibling.type = this.previousElementSibling.type === 'password' ? 'text' : 'password'"> onclick="this.previousElementSibling.type = this.previousElementSibling.type === 'password' ? 'text' : 'password'">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
<button class="btn btn-outline-primary" type="button" id="btn-probar-ia" onclick="probarIA()">
<i class="fas fa-plug me-1"></i>Probar
</button>
</div> </div>
<div class="form-text">El token se guarda cifrado y nunca se expone al navegador.</div> <div id="ia-test-result" class="form-text mt-1"></div>
</div> </div>
</div> </div>
@@ -311,6 +321,7 @@ async function guardar() {
doc_logo_base64: $('c-logo-base64').value, doc_logo_base64: $('c-logo-base64').value,
doc_pie_pagina: $('c-doc-pie').value.trim(), doc_pie_pagina: $('c-doc-pie').value.trim(),
gemini_api_key: $('c-gemini-key').value.trim(), gemini_api_key: $('c-gemini-key').value.trim(),
ia_activa: $('c-ia-activa').checked ? '1' : '0',
}; };
if (!payload.empresa_nombre) { if (!payload.empresa_nombre) {
@@ -342,6 +353,36 @@ async function guardar() {
btn.innerHTML = '<i class="fas fa-save me-2"></i>Guardar configuración'; btn.innerHTML = '<i class="fas fa-save me-2"></i>Guardar configuración';
} }
} }
async function probarIA() {
const key = $('c-gemini-key').value.trim();
const res = document.getElementById('ia-test-result');
const btn = document.getElementById('btn-probar-ia');
if (!key) { res.innerHTML = '<span class="text-danger">Ingresa el token primero.</span>'; return; }
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
res.innerHTML = '';
try {
const r = await fetch('api/lab/test_gemini.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key }),
});
const d = await r.json();
if (d.ok) {
res.innerHTML = '<span class="text-success"><i class="fas fa-check-circle me-1"></i>Conexión exitosa — ' + (d.modelo || 'Gemini') + '</span>';
} else {
res.innerHTML = '<span class="text-danger"><i class="fas fa-times-circle me-1"></i>' + (d.error || 'Error de conexión') + '</span>';
}
} catch(_) {
res.innerHTML = '<span class="text-danger"><i class="fas fa-times-circle me-1"></i>No se pudo conectar.</span>';
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plug me-1"></i>Probar';
}
}
</script> </script>
<script src="assets/js/lab-sidebar.js"></script> <script src="assets/js/lab-sidebar.js"></script>
</body> </body>
+8
View File
@@ -7,6 +7,12 @@
require_once APP_ROOT . '/config/config.php'; require_once APP_ROOT . '/config/config.php';
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; } if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
$_iaActiva = false;
try {
$_cfgIA = Database::getInstance()->fetch("SELECT valor FROM lab_config WHERE clave = 'ia_activa'");
$_iaActiva = ($_cfgIA['valor'] ?? '0') === '1';
} catch (\Throwable $_) {}
Layout::open('Dashboard Turnero', 'fas fa-chart-bar'); Layout::open('Dashboard Turnero', 'fas fa-chart-bar');
?> ?>
<style> <style>
@@ -191,6 +197,7 @@ Layout::open('Dashboard Turnero', 'fas fa-chart-bar');
</div> </div>
</div> </div>
<?php if ($_iaActiva): ?>
<!-- ── Botón flotante IA ── --> <!-- ── Botón flotante IA ── -->
<button id="btnAI" title="Asistente IA" onclick="toggleAI()"> <button id="btnAI" title="Asistente IA" onclick="toggleAI()">
<i class="fas fa-robot"></i> <i class="fas fa-robot"></i>
@@ -212,6 +219,7 @@ Layout::open('Dashboard Turnero', 'fas fa-chart-bar');
<button class="btn-send" onclick="enviarIA()"><i class="fas fa-paper-plane"></i></button> <button class="btn-send" onclick="enviarIA()"><i class="fas fa-paper-plane"></i></button>
</div> </div>
</div> </div>
<?php endif; ?>
<script> <script>
/* ═══════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════