feat(dashboard): asistente IA con voz, markdown y preguntas rápidas
- Audio input: botón micrófono con SpeechRecognition (es-CO) - Audio output: TTS con SpeechSynthesis, toggle para silenciar - Markdown: **negrita** → <strong>, * items → <ul> en respuestas - Chips de preguntas rápidas: Resumen, Facturación, Tiempos, En espera - Panel minimizable: clic en header colapsa/expande el cuerpo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
941b6740f0
commit
a8752b9e9d
@@ -89,28 +89,41 @@ Layout::open('Dashboard Turnero', 'fas fa-chart-bar');
|
||||
|
||||
/* ── Panel IA ── */
|
||||
#aiPanel {
|
||||
position:fixed; bottom:0; right:24px; width:360px; z-index:1080;
|
||||
position:fixed; bottom:0; right:24px; width:380px; z-index:1080;
|
||||
box-shadow:0 -4px 32px rgba(0,0,0,.18); border-radius:16px 16px 0 0;
|
||||
background:#fff; border:1px solid #e2e8f0; border-bottom:none;
|
||||
transform:translateY(100%); transition:transform .3s ease;
|
||||
display:flex; flex-direction:column; max-height:520px;
|
||||
display:flex; flex-direction:column; max-height:560px;
|
||||
}
|
||||
#aiPanel.open { transform:translateY(0); }
|
||||
#aiPanel.minimized .ai-body { display:none; }
|
||||
#aiPanel.minimized { max-height:none; }
|
||||
.ai-header {
|
||||
background:linear-gradient(135deg, var(--brand-dark,#0d47a1), var(--brand,#1565c0));
|
||||
color:#fff; padding:12px 16px; border-radius:16px 16px 0 0;
|
||||
display:flex; align-items:center; gap:8px; flex-shrink:0;
|
||||
display:flex; align-items:center; gap:8px; flex-shrink:0; cursor:pointer; user-select:none;
|
||||
}
|
||||
.ai-header .ai-title { flex:1; font-weight:700; font-size:.92rem; }
|
||||
.ai-header .btn-close { filter:invert(1) brightness(2); }
|
||||
.btn-tts { background:transparent; border:none; color:rgba(255,255,255,.55); cursor:pointer; padding:3px 6px; border-radius:6px; font-size:.85rem; transition:color .2s; }
|
||||
.btn-tts.active { color:#fff; }
|
||||
.ai-body { display:flex; flex-direction:column; flex:1; overflow:hidden; }
|
||||
.ai-quick { display:flex; gap:6px; flex-wrap:wrap; padding:8px 12px; border-bottom:1px solid #f1f5f9; flex-shrink:0; }
|
||||
.ai-chip { background:#f1f5f9; border:1px solid #e2e8f0; border-radius:99px; padding:4px 10px; font-size:.73rem; cursor:pointer; color:#475569; white-space:nowrap; transition:background .15s; }
|
||||
.ai-chip:hover { background:#e2e8f0; }
|
||||
.ai-messages { flex:1; overflow-y:auto; padding:12px 14px; display:flex; flex-direction:column; gap:8px; }
|
||||
.ai-msg { max-width:88%; padding:8px 12px; border-radius:12px; font-size:.83rem; line-height:1.45; }
|
||||
.ai-msg { max-width:90%; padding:8px 12px; border-radius:12px; font-size:.83rem; line-height:1.5; }
|
||||
.ai-msg.user { background:var(--brand,#1565c0); color:#fff; align-self:flex-end; border-radius:12px 12px 2px 12px; }
|
||||
.ai-msg.bot { background:#f1f5f9; color:#1e293b; align-self:flex-start; border-radius:12px 12px 12px 2px; }
|
||||
.ai-msg.bot.typing { color:#94a3b8; font-style:italic; }
|
||||
.ai-footer { padding:10px 12px; border-top:1px solid #e2e8f0; display:flex; gap:8px; flex-shrink:0; }
|
||||
.ai-msg.bot strong { color:#0d47a1; }
|
||||
.ai-msg.bot ul { margin:4px 0 4px 14px; padding:0; }
|
||||
.ai-footer { padding:10px 12px; border-top:1px solid #e2e8f0; display:flex; gap:6px; flex-shrink:0; }
|
||||
.ai-footer input { flex:1; border:1px solid #e2e8f0; border-radius:8px; padding:7px 12px; font-size:.85rem; outline:none; }
|
||||
.ai-footer input:focus { border-color:var(--brand,#1565c0); }
|
||||
.btn-mic { background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:7px 11px; cursor:pointer; color:#64748b; font-size:.85rem; transition:all .2s; }
|
||||
.btn-mic.listening { background:#fee2e2; color:#dc2626; border-color:#fca5a5; animation:mic-pulse 1s infinite; }
|
||||
@keyframes mic-pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
|
||||
.ai-footer .btn-send { background:var(--brand,#1565c0); color:#fff; border:none; border-radius:8px; padding:7px 14px; cursor:pointer; font-size:.85rem; }
|
||||
#btnAI {
|
||||
position:fixed; bottom:24px; right:24px; z-index:1079;
|
||||
@@ -260,18 +273,30 @@ try {
|
||||
|
||||
<!-- ── Panel IA ── -->
|
||||
<div id="aiPanel">
|
||||
<div class="ai-header">
|
||||
<div class="ai-header" onclick="minimizarIA(event)">
|
||||
<i class="fas fa-robot"></i>
|
||||
<span class="ai-title">Asistente del Turnero</span>
|
||||
<button class="btn-close" onclick="toggleAI()"></button>
|
||||
<button id="btnTTS" class="btn-tts active" onclick="event.stopPropagation();toggleTTS()" title="Activar/silenciar voz">
|
||||
<i class="fas fa-volume-up"></i>
|
||||
</button>
|
||||
<button class="btn-close" onclick="event.stopPropagation();toggleAI()"></button>
|
||||
</div>
|
||||
<div class="ai-messages" id="aiMessages">
|
||||
<div class="ai-msg bot">¡Hola! Puedo responder preguntas sobre el estado del turnero de hoy. ¿Qué necesitas saber?</div>
|
||||
</div>
|
||||
<div class="ai-footer">
|
||||
<input type="text" id="aiInput" placeholder="¿Cuántos pacientes en espera?…" maxlength="400"
|
||||
onkeydown="if(event.key==='Enter')enviarIA()">
|
||||
<button class="btn-send" onclick="enviarIA()"><i class="fas fa-paper-plane"></i></button>
|
||||
<div class="ai-body" id="aiBody">
|
||||
<div class="ai-quick" id="aiQuick">
|
||||
<button class="ai-chip" onclick="enviarRapido('¿Cuántos pacientes atendidos hoy y cuántos faltan?')">📊 Resumen</button>
|
||||
<button class="ai-chip" onclick="enviarRapido('¿Cuánto se ha facturado hoy?')">💰 Facturación</button>
|
||||
<button class="ai-chip" onclick="enviarRapido('¿Cuáles son los tiempos promedio de hoy?')">⏱ Tiempos</button>
|
||||
<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 class="ai-msg bot">¡Hola! Pregúntame sobre el turnero de hoy o usa los botones de arriba. También puedes hablar 🎤</div>
|
||||
</div>
|
||||
<div class="ai-footer">
|
||||
<input type="text" id="aiInput" placeholder="Escribe o toca el micrófono…" maxlength="400"
|
||||
onkeydown="if(event.key==='Enter')enviarIA()">
|
||||
<button class="btn-mic" id="btnMic" onclick="iniciarVoz()" title="Hablar"><i class="fas fa-microphone"></i></button>
|
||||
<button class="btn-send" onclick="enviarIA()"><i class="fas fa-paper-plane"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -568,11 +593,74 @@ function filtrarTabla() {
|
||||
// ── Asistente IA ──────────────────────────────────────────────
|
||||
let _aiOpen = false;
|
||||
|
||||
// ── IA helpers ───────────────────────────────────────────────
|
||||
let _aiMinimized = false;
|
||||
let _ttsOn = true;
|
||||
let _recog = null;
|
||||
|
||||
function mdToHtml(md) {
|
||||
let s = md
|
||||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
|
||||
.replace(/^\* (.+)$/gm,'<li>$1</li>');
|
||||
s = s.replace(/((?:<li>[^]*?<\/li>\n?)+)/g,
|
||||
m => `<ul style="margin:4px 0 4px 14px;padding:0;list-style:disc">${m.replace(/\n/g,'')}</ul>`);
|
||||
return s.replace(/\n/g,'<br>');
|
||||
}
|
||||
|
||||
function _stripMd(text) {
|
||||
return text.replace(/\*\*(.+?)\*\*/g,'$1').replace(/^\* /gm,'').replace(/\n/g,' ');
|
||||
}
|
||||
|
||||
function hablarIA(text) {
|
||||
if (!_ttsOn || !window.speechSynthesis) return;
|
||||
window.speechSynthesis.cancel();
|
||||
const utt = new SpeechSynthesisUtterance(_stripMd(text));
|
||||
utt.lang = 'es-CO'; utt.rate = 1.05;
|
||||
window.speechSynthesis.speak(utt);
|
||||
}
|
||||
|
||||
function toggleTTS() {
|
||||
_ttsOn = !_ttsOn;
|
||||
document.getElementById('btnTTS').classList.toggle('active', _ttsOn);
|
||||
if (!_ttsOn) window.speechSynthesis?.cancel();
|
||||
}
|
||||
|
||||
function iniciarVoz() {
|
||||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
if (!SR) { alert('Tu navegador no soporta reconocimiento de voz (usa Chrome)'); return; }
|
||||
const btn = document.getElementById('btnMic');
|
||||
if (_recog) { _recog.stop(); return; }
|
||||
_recog = new SR();
|
||||
_recog.lang = 'es-CO'; _recog.interimResults = false;
|
||||
btn.classList.add('listening');
|
||||
_recog.onresult = e => {
|
||||
document.getElementById('aiInput').value = e.results[0][0].transcript;
|
||||
enviarIA();
|
||||
};
|
||||
_recog.onend = () => { btn.classList.remove('listening'); _recog = null; };
|
||||
_recog.onerror= () => { btn.classList.remove('listening'); _recog = null; };
|
||||
_recog.start();
|
||||
}
|
||||
|
||||
function enviarRapido(pregunta) {
|
||||
document.getElementById('aiInput').value = pregunta;
|
||||
document.getElementById('aiQuick').style.display = 'none';
|
||||
enviarIA();
|
||||
}
|
||||
|
||||
function minimizarIA(e) {
|
||||
if (e.target.closest('button')) return;
|
||||
_aiMinimized = !_aiMinimized;
|
||||
document.getElementById('aiPanel').classList.toggle('minimized', _aiMinimized);
|
||||
}
|
||||
|
||||
function toggleAI() {
|
||||
_aiOpen = !_aiOpen;
|
||||
if (!_aiOpen) { _aiMinimized = false; document.getElementById('aiPanel').classList.remove('minimized'); }
|
||||
document.getElementById('aiPanel').classList.toggle('open', _aiOpen);
|
||||
document.getElementById('btnAI').classList.toggle('hidden', _aiOpen);
|
||||
if (_aiOpen) document.getElementById('aiInput').focus();
|
||||
if (_aiOpen && !_aiMinimized) document.getElementById('aiInput').focus();
|
||||
}
|
||||
|
||||
async function enviarIA() {
|
||||
@@ -599,7 +687,8 @@ async function enviarIA() {
|
||||
});
|
||||
const d = await res.json();
|
||||
typing.className = 'ai-msg bot';
|
||||
typing.textContent = d.ok ? d.respuesta : (d.error || 'Error al conectar con la IA');
|
||||
if (d.ok) { typing.innerHTML = mdToHtml(d.respuesta); hablarIA(d.respuesta); }
|
||||
else { typing.textContent = d.error || 'Error al conectar con la IA'; }
|
||||
} catch(_) {
|
||||
typing.className = 'ai-msg bot';
|
||||
typing.textContent = 'Error de conexión';
|
||||
|
||||
Reference in New Issue
Block a user