feat(LIA): barra de tokens 1M con bloqueo al agotarse
- ai_chat.php: cuenta tokens reales desde Gemini usageMetadata, persiste en lab_config, bloquea con HTTP 402 al agotar 1.000.000 - dashboard.php: _actualizarTokens() actualiza barra visual, _bloquearLIA() deshabilita input+mic y muestra aviso de soporte Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
34d0fed21b
commit
320b47797a
@@ -14,12 +14,19 @@ $pregunta = trim($body['pregunta'] ?? '');
|
||||
if ($pregunta === '') jsonError('Pregunta vacía', 400);
|
||||
if (mb_strlen($pregunta) > 800) jsonError('Pregunta demasiado larga', 400);
|
||||
|
||||
define('LIA_TOKENS_MAX', 1_000_000);
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// ── Clave Gemini ──────────────────────────────────────────────
|
||||
$row = $pdo->query("SELECT valor FROM lab_config WHERE clave = 'gemini_api_key' LIMIT 1")->fetch();
|
||||
$apiKey = trim($row['valor'] ?? '');
|
||||
$cfg = $pdo->query("SELECT clave, valor FROM lab_config WHERE clave IN ('gemini_api_key','lia_tokens_usados')")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$apiKey = trim($cfg['gemini_api_key'] ?? '');
|
||||
$tokensUsados = (int)($cfg['lia_tokens_usados'] ?? 0);
|
||||
|
||||
if (!$apiKey) jsonError('Token de IA no configurado. Ve a Configuración del laboratorio.', 503);
|
||||
if ($tokensUsados >= LIA_TOKENS_MAX) {
|
||||
jsonError('⚠️ Se agotaron los tokens de LIA. Solicita tokens a soporte para continuar.', 402);
|
||||
}
|
||||
|
||||
// ── Contexto del día ──────────────────────────────────────────
|
||||
$hoy = date('Y-m-d');
|
||||
@@ -207,5 +214,17 @@ if ($code !== 200 || empty($gemini['candidates'][0]['content']['parts'][0]['text
|
||||
jsonError("Error IA: {$detail}", 502);
|
||||
}
|
||||
|
||||
$respuesta = $gemini['candidates'][0]['content']['parts'][0]['text'];
|
||||
jsonOk(['respuesta' => $respuesta]);
|
||||
$respuesta = $gemini['candidates'][0]['content']['parts'][0]['text'];
|
||||
$usage = $gemini['usageMetadata'] ?? [];
|
||||
$tokensEsta = (int)($usage['totalTokenCount'] ?? (int)((mb_strlen($pregunta) + mb_strlen($respuesta)) / 4));
|
||||
$tokensUsados += $tokensEsta;
|
||||
|
||||
$pdo->prepare("INSERT INTO lab_config (clave, valor) VALUES ('lia_tokens_usados', ?)
|
||||
ON DUPLICATE KEY UPDATE valor = ?")->execute([$tokensUsados, $tokensUsados]);
|
||||
|
||||
jsonOk([
|
||||
'respuesta' => $respuesta,
|
||||
'tokens_usados' => $tokensUsados,
|
||||
'tokens_restantes' => max(0, LIA_TOKENS_MAX - $tokensUsados),
|
||||
'tokens_max' => LIA_TOKENS_MAX,
|
||||
]);
|
||||
|
||||
@@ -140,6 +140,11 @@ Layout::open('Dashboard Turnero', 'fas fa-chart-bar');
|
||||
.lia-wave span:nth-child(4){height:16px;animation-delay:.3s}
|
||||
.lia-wave span:nth-child(5){height:8px;animation-delay:.4s}
|
||||
@keyframes wave { 0%,100%{transform:scaleY(.5);opacity:.6} 50%{transform:scaleY(1);opacity:1} }
|
||||
.lia-tokens { padding:6px 14px; background:#f8faff; border-top:1px solid #f1f5f9; flex-shrink:0; }
|
||||
.lia-tokens-bar { height:5px; border-radius:3px; background:#e2e8f0; overflow:hidden; margin-bottom:3px; }
|
||||
.lia-tokens-fill { height:100%; border-radius:3px; background:linear-gradient(90deg,#1565c0,#0288d1); transition:width .6s ease; }
|
||||
.lia-tokens-fill.warn { background:linear-gradient(90deg,#f59e0b,#ef4444); }
|
||||
.lia-tokens-label { font-size:.68rem; color:#94a3b8; display:flex; justify-content:space-between; }
|
||||
.ai-footer { padding:10px 14px; border-top:1px solid #f1f5f9; display:flex; gap:8px; flex-shrink:0; background:#fafbff; }
|
||||
.ai-footer input { flex:1; border:1.5px solid #e2e8f0; border-radius:10px; padding:9px 14px; font-size:.9rem; outline:none; background:#fff; }
|
||||
.ai-footer input:focus { border-color:#1565c0; }
|
||||
@@ -352,6 +357,13 @@ try {
|
||||
<button class="ai-chip" onclick="enviarRapido('¿Cuántos pacientes están en espera ahora mismo?')">⏳ En espera</button>
|
||||
</div>
|
||||
<div class="ai-messages" id="aiMessages"></div>
|
||||
<div class="lia-tokens" id="liaTokensBar">
|
||||
<div class="lia-tokens-bar"><div class="lia-tokens-fill" id="liaFill" style="width:0%"></div></div>
|
||||
<div class="lia-tokens-label">
|
||||
<span id="liaTokensUsed">0 tokens usados</span>
|
||||
<span id="liaTokensLeft">1.000.000 restantes</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="liaListening" style="display:none;text-align:center;padding:10px 0">
|
||||
<div class="lia-wave"><span></span><span></span><span></span><span></span><span></span></div>
|
||||
<div style="font-size:.75rem;color:#64748b;margin-top:4px">Escuchando…</div>
|
||||
@@ -669,6 +681,31 @@ let _ttsOn = true;
|
||||
let _recog = null;
|
||||
let _chartProy = null;
|
||||
|
||||
function _actualizarTokens(usados, max) {
|
||||
const pct = Math.min(100, (usados / max) * 100);
|
||||
const left = max - usados;
|
||||
const fill = document.getElementById('liaFill');
|
||||
const fmt = n => Intl.NumberFormat('es-CO').format(n);
|
||||
fill.style.width = pct + '%';
|
||||
fill.className = 'lia-tokens-fill' + (pct > 80 ? ' warn' : '');
|
||||
document.getElementById('liaTokensUsed').textContent = fmt(usados) + ' tokens usados';
|
||||
document.getElementById('liaTokensLeft').textContent = fmt(left) + ' restantes';
|
||||
if (left <= 0) _bloquearLIA();
|
||||
}
|
||||
|
||||
function _bloquearLIA() {
|
||||
document.getElementById('aiInput').disabled = true;
|
||||
document.getElementById('btnMic').disabled = true;
|
||||
document.getElementById('btnMic').style.opacity = '.4';
|
||||
const msgs = document.getElementById('aiMessages');
|
||||
const el = document.createElement('div');
|
||||
el.className = 'ai-msg bot';
|
||||
el.innerHTML = '⚠️ <strong>Se agotaron los tokens de LIA.</strong><br>Solicita tokens a soporte para continuar.';
|
||||
msgs.appendChild(el);
|
||||
msgs.scrollTop = msgs.scrollHeight;
|
||||
hablarIA('Se agotaron los tokens de LIA. Solicita tokens a soporte para continuar.');
|
||||
}
|
||||
|
||||
function mdToHtml(md) {
|
||||
let s = md
|
||||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||
@@ -820,8 +857,14 @@ async function enviarIA() {
|
||||
});
|
||||
const d = await res.json();
|
||||
typing.className = 'ai-msg bot';
|
||||
if (d.ok) { typing.innerHTML = mdToHtml(d.respuesta); hablarIA(d.respuesta); }
|
||||
else { typing.textContent = d.error || 'Error al conectar con la IA'; }
|
||||
if (d.ok) {
|
||||
typing.innerHTML = mdToHtml(d.respuesta);
|
||||
hablarIA(d.respuesta);
|
||||
if (d.tokens_max) _actualizarTokens(d.tokens_usados, d.tokens_max);
|
||||
} else {
|
||||
typing.textContent = d.error || 'Error al conectar con la IA';
|
||||
if (d.status === 402 || (d.error && d.error.includes('agotaron'))) _bloquearLIA();
|
||||
}
|
||||
} catch(_) {
|
||||
typing.className = 'ai-msg bot';
|
||||
typing.textContent = 'Error de conexión';
|
||||
|
||||
Reference in New Issue
Block a user