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',
'doc_color', 'doc_logo_base64', 'doc_pie_pagina',
'gemini_api_key',
'ia_activa',
];
$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 ────────────────────────── -->
<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">
El asistente IA del turnero usa Google Gemini Flash para responder preguntas
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'">
<i class="fas fa-eye"></i>
</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 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>
@@ -311,6 +321,7 @@ async function guardar() {
doc_logo_base64: $('c-logo-base64').value,
doc_pie_pagina: $('c-doc-pie').value.trim(),
gemini_api_key: $('c-gemini-key').value.trim(),
ia_activa: $('c-ia-activa').checked ? '1' : '0',
};
if (!payload.empresa_nombre) {
@@ -342,6 +353,36 @@ async function guardar() {
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 src="assets/js/lab-sidebar.js"></script>
</body>
+8
View File
@@ -7,6 +7,12 @@
require_once APP_ROOT . '/config/config.php';
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');
?>
<style>
@@ -191,6 +197,7 @@ Layout::open('Dashboard Turnero', 'fas fa-chart-bar');
</div>
</div>
<?php if ($_iaActiva): ?>
<!-- ── Botón flotante IA ── -->
<button id="btnAI" title="Asistente IA" onclick="toggleAI()">
<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>
</div>
</div>
<?php endif; ?>
<script>
/* ═══════════════════════════════════════════════════════════