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',
|
'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;
|
||||||
|
|||||||
@@ -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 ────────────────────────── -->
|
<!-- ── 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
/* ═══════════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user