fix: save gemini keys in settings + add AI connection test

- Add gemini_api_key and gemini_model to allowed keys in settings save handler
- Add POST /admin/settings/test-ai endpoint for testing Gemini/OpenAI connection
- Add "Probar conexion IA" card in settings page with live feedback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-22 11:34:25 -05:00
co-authored by Claude Sonnet 4.6
parent 5277d47c14
commit c702dbadc1
2 changed files with 66 additions and 1 deletions
+34
View File
@@ -1750,8 +1750,42 @@ HTML;
<button type="submit" class="btn-primary">Guardar configuración</button>
</div>
</form>
<div class="card" style="margin-top:16px">
<div class="card-h">Probar conexión IA</div>
<div class="card-b">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
<button type="button" class="btn-primary" onclick="testAiConn()" id="btnTestAi">Probar conexión</button>
<span id="aiTestResult" style="font-size:13px"></span>
</div>
<small style="margin-top:8px;color:#888">Guarda primero la configuración, luego prueba la conexión. Se enviará un mensaje de prueba al proveedor activo.</small>
</div>
</div>
<div class="version-info">Los cambios se aplican inmediatamente. Los valores se almacenan en la base de datos.</div>
</div>
<script>
function testAiConn() {
var btn = document.getElementById('btnTestAi');
var res = document.getElementById('aiTestResult');
btn.disabled = true;
res.textContent = 'Probando...';
res.style.color = '#666';
fetch('/admin/settings/test-ai', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:''})
.then(function(r){return r.json();})
.then(function(d){
if (d.ok) {
res.textContent = 'Conexion exitosa (' + d.provider + '): ' + d.response;
res.style.color = '#16a34a';
} else {
res.textContent = 'Error (' + d.provider + '): ' + d.error;
res.style.color = '#dc2626';
}
})
.catch(function(e){ res.textContent = 'Error de red'; res.style.color = '#dc2626'; })
.finally(function(){ btn.disabled = false; });
}
</script>
</body>
</html>
HTML;
+32 -1
View File
@@ -415,7 +415,7 @@ $routes = [
$allowed = [
'whatsapp_access_token', 'whatsapp_app_secret', 'whatsapp_verify_token',
'whatsapp_business_account_id', 'whatsapp_default_phone_number_id',
'ai_provider', 'openai_api_key', 'openai_model', 'ai_max_tokens', 'ai_default_prompt',
'ai_provider', 'openai_api_key', 'openai_model', 'gemini_api_key', 'gemini_model', 'ai_max_tokens', 'ai_default_prompt',
'verify_hmac_signature',
];
// Checkbox: si no está presente, desactivar
@@ -443,6 +443,37 @@ $routes = [
exit;
})()],
['POST', '/admin/settings/test-ai', fn() => (function () {
SessionAuth::require();
$provider = Settings::get('ai_provider', 'mock');
if ($provider === 'gemini') {
$apiKey = Settings::get('gemini_api_key', '');
$model = Settings::get('gemini_model', 'gemini-2.0-flash');
if ($apiKey === '') { jsonResponse(200, ['ok' => false, 'provider' => 'gemini', 'error' => 'API Key no configurada. Guarda primero.']); return; }
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
$payload = json_encode(['contents' => [['role' => 'user', 'parts' => [['text' => 'Responde solo: OK']]]]]);
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15]);
$resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch);
if ($code !== 200) { $msg = $err ?: (json_decode($resp, true)['error']['message'] ?? "HTTP {$code}"); jsonResponse(200, ['ok' => false, 'provider' => 'gemini', 'error' => $msg]); return; }
$text = json_decode($resp, true)['candidates'][0]['content']['parts'][0]['text'] ?? '(sin respuesta)';
jsonResponse(200, ['ok' => true, 'provider' => 'gemini', 'response' => trim($text)]);
} elseif ($provider === 'openai') {
$apiKey = Settings::get('openai_api_key', '');
$model = Settings::get('openai_model', 'gpt-4o-mini');
if ($apiKey === '') { jsonResponse(200, ['ok' => false, 'provider' => 'openai', 'error' => 'API Key no configurada. Guarda primero.']); return; }
$payload = json_encode(['model' => $model, 'messages' => [['role' => 'user', 'content' => 'Responde solo: OK']], 'max_tokens' => 5]);
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $apiKey], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15]);
$resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch);
if ($code !== 200) { $msg = $err ?: (json_decode($resp, true)['error']['message'] ?? "HTTP {$code}"); jsonResponse(200, ['ok' => false, 'provider' => 'openai', 'error' => $msg]); return; }
$text = json_decode($resp, true)['choices'][0]['message']['content'] ?? '(sin respuesta)';
jsonResponse(200, ['ok' => true, 'provider' => 'openai', 'response' => trim($text)]);
} else {
jsonResponse(200, ['ok' => true, 'provider' => 'mock', 'response' => 'Mock activo — no hay proveedor real configurado.']);
}
})()],
// ─── Debug: captura cruda de webhooks (loguea todo sin validar) ───────
['GET', '/admin/v1/wp-webhook-debug', fn() => (function () {
$log = '[' . date('Y-m-d H:i:s') . "] GET " . ($_SERVER['QUERY_STRING'] ?? '') . "\n";