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:
co-authored by
Claude Sonnet 4.6
parent
3b5c26214c
commit
cd3d3bb9c0
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user