Documentación en usted, y panel de LIA con el mismo diseño del dashboard
Toda la documentación pasa de voseo a tratamiento de usted, incluidos los diagramas y los textos de la interfaz. Los prompts de ambos asistentes lo piden explícitamente, para que las respuestas generadas también lo respeten. El panel de LIA en la página de documentación adopta el diseño del dashboard del turnero: mismo botón circular, mismo panel deslizante con encabezado en degradado y logo LIA, mismos chips de atajo, burbujas y campo de entrada. Los atajos se arman con los documentos que ese usuario puede ver. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
71c9dfd424
commit
fe96e5b60d
@@ -135,47 +135,85 @@ Layout::open('Soporte · Documentación', 'fas fa-life-ring');
|
||||
.doc-body { padding:20px 18px 60px; }
|
||||
}
|
||||
@media print {
|
||||
.doc-nav, .doc-toc, .doc-ruta, .lia-fab, .lia-panel { display:none !important; }
|
||||
.doc-nav, .doc-toc, .doc-ruta, #btnAI, #aiPanel { display:none !important; }
|
||||
.doc-body { max-width:none; padding:0; }
|
||||
.doc-body pre { background:#f8fafc; color:#0f172a; border:1px solid #cbd5e1; }
|
||||
}
|
||||
|
||||
/* ── Asistente LIA ── */
|
||||
.lia-fab { position:fixed; right:22px; bottom:22px; z-index:1040; width:54px; height:54px;
|
||||
border-radius:50%; border:none; cursor:pointer; color:#fff; font-size:1.25rem;
|
||||
background:linear-gradient(135deg,#2563eb,#7c3aed); box-shadow:0 8px 24px rgba(37,99,235,.4); }
|
||||
.lia-fab:hover { transform:scale(1.06); }
|
||||
.lia-panel { position:fixed; right:22px; bottom:88px; z-index:1041; width:390px; max-width:calc(100vw - 44px);
|
||||
height:520px; max-height:calc(100vh - 130px); background:#fff; border:1px solid #e2e8f0;
|
||||
border-radius:16px; box-shadow:0 20px 60px rgba(0,0,0,.18); display:none;
|
||||
flex-direction:column; overflow:hidden; }
|
||||
.lia-panel.abierto { display:flex; }
|
||||
.lia-head { padding:12px 15px; background:linear-gradient(135deg,#2563eb,#7c3aed); color:#fff;
|
||||
display:flex; align-items:center; gap:9px; }
|
||||
.lia-head .t { font-weight:700; font-size:.92rem; flex:1; }
|
||||
.lia-head .s { font-size:.7rem; opacity:.85; display:block; font-weight:400; }
|
||||
.lia-head button { background:none; border:none; color:#fff; cursor:pointer; font-size:1rem; opacity:.85; }
|
||||
.lia-msgs { flex:1; overflow-y:auto; padding:14px; background:#f8fafc; }
|
||||
.lia-msg { max-width:88%; padding:9px 12px; border-radius:12px; font-size:.84rem; line-height:1.55;
|
||||
margin-bottom:9px; word-wrap:break-word; }
|
||||
.lia-msg.user { background:#2563eb; color:#fff; margin-left:auto; border-bottom-right-radius:3px; }
|
||||
.lia-msg.bot { background:#fff; color:#1e293b; border:1px solid #e2e8f0; border-bottom-left-radius:3px; }
|
||||
.lia-msg.bot table { width:100%; border-collapse:collapse; font-size:.76rem; margin:6px 0; }
|
||||
.lia-msg.bot th, .lia-msg.bot td { border:1px solid #e2e8f0; padding:4px 6px; text-align:left; }
|
||||
.lia-msg.bot code { background:#f1f5f9; color:#be185d; padding:1px 4px; border-radius:3px; font-size:.9em; }
|
||||
.lia-msg.bot ul, .lia-msg.bot ol { padding-left:18px; margin:6px 0; }
|
||||
.lia-fuentes { font-size:.68rem; color:#64748b; margin:-4px 0 10px 2px; }
|
||||
.lia-fuentes a { color:#2563eb; text-decoration:none; }
|
||||
.lia-in { display:flex; gap:7px; padding:11px; border-top:1px solid #e2e8f0; background:#fff; }
|
||||
.lia-in input { flex:1; border:1px solid #cbd5e1; border-radius:9px; padding:8px 11px; font-size:.84rem; }
|
||||
.lia-in input:focus { outline:none; border-color:#2563eb; }
|
||||
.lia-in button { border:none; background:#2563eb; color:#fff; border-radius:9px; width:38px; cursor:pointer; }
|
||||
.lia-in button:disabled { opacity:.5; cursor:default; }
|
||||
.lia-sug { padding:0 14px 12px; }
|
||||
.lia-sug button { display:block; width:100%; text-align:left; background:#fff; border:1px solid #e2e8f0;
|
||||
border-radius:9px; padding:7px 11px; font-size:.79rem; color:#334155; cursor:pointer;
|
||||
margin-bottom:6px; }
|
||||
.lia-sug button:hover { border-color:#2563eb; color:#1d4ed8; }
|
||||
/* ── Panel LIA — mismo diseño que el dashboard del turnero ── */
|
||||
#aiPanel {
|
||||
position:fixed; bottom:0; right:24px; width:400px; z-index:1080;
|
||||
box-shadow:0 -8px 40px rgba(0,0,0,.22); border-radius:20px 20px 0 0;
|
||||
background:#fff; border:1px solid #e2e8f0; border-bottom:none;
|
||||
transform:translateY(100%); transition:transform .35s cubic-bezier(.4,0,.2,1);
|
||||
display:flex; flex-direction:column; max-height:580px;
|
||||
}
|
||||
#aiPanel.open { transform:translateY(0); }
|
||||
#aiPanel.minimized .ai-body { display:none; }
|
||||
#aiPanel.minimized { max-height:none; }
|
||||
.ai-header {
|
||||
background:linear-gradient(135deg,#0d47a1,#1565c0,#0288d1);
|
||||
color:#fff; padding:14px 18px; border-radius:20px 20px 0 0;
|
||||
display:flex; align-items:center; gap:12px; flex-shrink:0; cursor:pointer; user-select:none;
|
||||
}
|
||||
.lia-logo {
|
||||
width:44px; height:44px; border-radius:50%;
|
||||
background:rgba(255,255,255,.15); border:2px solid rgba(255,255,255,.4);
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
font-size:1rem; font-weight:900; letter-spacing:-1px; flex-shrink:0;
|
||||
transition:transform .3s;
|
||||
}
|
||||
.lia-logo:hover { transform:scale(1.08); }
|
||||
.lia-logo .lia-i { color:#7dd3fc; }
|
||||
.ai-title { font-weight:800; font-size:1.05rem; letter-spacing:.5px; }
|
||||
.btn-tts { background:rgba(255,255,255,.15); border:1px solid rgba(255,255,255,.3);
|
||||
color:rgba(255,255,255,.7); cursor:pointer; padding:5px 9px; border-radius:8px;
|
||||
font-size:.88rem; transition:all .2s; }
|
||||
.btn-tts:hover { background:rgba(255,255,255,.25); color:#fff; }
|
||||
.ai-header .btn-close { filter:invert(1) brightness(2); opacity:.7; }
|
||||
.ai-header .btn-close:hover { opacity:1; }
|
||||
.ai-body { display:flex; flex-direction:column; flex:1; overflow:hidden; }
|
||||
.ai-quick { display:flex; gap:7px; flex-wrap:wrap; padding:10px 14px;
|
||||
border-bottom:1px solid #f1f5f9; flex-shrink:0; background:#fafbff; }
|
||||
.ai-chip { background:#fff; border:1.5px solid #e2e8f0; border-radius:99px; padding:6px 13px;
|
||||
font-size:.82rem; cursor:pointer; color:#334155; white-space:nowrap;
|
||||
transition:all .15s; font-weight:500; }
|
||||
.ai-chip:hover { background:#eff6ff; border-color:#93c5fd; color:#1d4ed8; }
|
||||
.ai-messages { flex:1; overflow-y:auto; padding:14px 16px; display:flex; flex-direction:column; gap:10px; }
|
||||
.ai-msg { max-width:92%; padding:10px 14px; border-radius:16px; font-size:.9rem;
|
||||
line-height:1.55; animation:msg-in .2s ease; word-wrap:break-word; }
|
||||
@keyframes msg-in { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:none} }
|
||||
.ai-msg.user { background:linear-gradient(135deg,#1565c0,#0288d1); color:#fff;
|
||||
align-self:flex-end; border-radius:16px 16px 2px 16px; }
|
||||
.ai-msg.bot { background:#f1f5f9; color:#1e293b; align-self:flex-start; border-radius:16px 16px 16px 2px; }
|
||||
.ai-msg.bot.typing::after { content:'●●●'; animation:dots 1.2s infinite; letter-spacing:2px; }
|
||||
@keyframes dots { 0%,100%{opacity:.3} 50%{opacity:1} }
|
||||
.ai-msg.bot strong { color:#1565c0; }
|
||||
.ai-msg.bot ul, .ai-msg.bot ol { margin:4px 0 4px 16px; padding:0; }
|
||||
.ai-msg.bot code { background:#e2e8f0; color:#be185d; padding:1px 4px; border-radius:3px; font-size:.9em; }
|
||||
.ai-msg.bot pre { background:#0f172a; color:#e2e8f0; padding:9px 11px; border-radius:8px;
|
||||
overflow-x:auto; font-size:.76rem; margin:6px 0; }
|
||||
.ai-fuentes { font-size:.68rem; color:#94a3b8; align-self:flex-start; margin:-6px 0 0 4px; }
|
||||
.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; }
|
||||
.ai-footer .btn-send { background:linear-gradient(135deg,#1565c0,#0288d1); color:#fff; border:none;
|
||||
border-radius:10px; padding:9px 16px; cursor:pointer; font-size:.88rem; min-width:42px; }
|
||||
.ai-footer .btn-send:disabled { opacity:.5; cursor:default; }
|
||||
#btnAI {
|
||||
position:fixed; bottom:24px; right:24px; z-index:1079;
|
||||
background:linear-gradient(135deg,#0d47a1,#1565c0,#0288d1);
|
||||
color:#fff; border:none; border-radius:50%; width:58px; height:58px;
|
||||
font-size:.78rem; font-weight:900; letter-spacing:-.5px;
|
||||
cursor:pointer; box-shadow:0 4px 20px rgba(21,101,192,.45);
|
||||
transition:transform .2s,box-shadow .2s;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
}
|
||||
#btnAI:hover { transform:scale(1.1); box-shadow:0 6px 28px rgba(21,101,192,.55); }
|
||||
#btnAI.hidden { display:none; }
|
||||
@media(max-width:600px) { #aiPanel { width:100%; right:0; border-radius:16px 16px 0 0; } }
|
||||
</style>
|
||||
|
||||
<div class="doc-wrap">
|
||||
@@ -269,23 +307,30 @@ Layout::open('Soporte · Documentación', 'fas fa-life-ring');
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Asistente LIA sobre la documentación ── -->
|
||||
<button class="lia-fab" id="liaFab" title="Preguntar a LIA"><i class="fas fa-robot"></i></button>
|
||||
<div class="lia-panel" id="liaPanel">
|
||||
<div class="lia-head">
|
||||
<i class="fas fa-robot"></i>
|
||||
<div class="t">LIA
|
||||
<span class="s">Responde sobre la documentación que podés ver</span>
|
||||
<!-- ── Panel LIA — asistente sobre la documentación ── -->
|
||||
<button id="btnAI" onclick="toggleAI()" title="Preguntar a LIA">LIA</button>
|
||||
<div id="aiPanel">
|
||||
<div class="ai-header" onclick="minimizarIA(event)">
|
||||
<div class="lia-logo" id="liaLogo">
|
||||
<span>L</span><span class="lia-i">I</span><span>A</span>
|
||||
</div>
|
||||
<button onclick="liaToggle()" title="Cerrar"><i class="fas fa-times"></i></button>
|
||||
<div style="flex:1;line-height:1.1">
|
||||
<div class="ai-title">LIA</div>
|
||||
<div style="font-size:.68rem;opacity:.75;font-weight:400">Asistente de Documentación</div>
|
||||
</div>
|
||||
<button class="btn-tts" onclick="event.stopPropagation();minimizarIA(event)" title="Minimizar" id="btnMin">
|
||||
<i class="fas fa-minus" id="iconMin"></i>
|
||||
</button>
|
||||
<button class="btn-close" onclick="event.stopPropagation();toggleAI()"></button>
|
||||
</div>
|
||||
<div class="lia-msgs" id="liaMsgs">
|
||||
<div class="lia-msg bot">Hola. Preguntame cómo hacer algo en el sistema y te respondo con la documentación.</div>
|
||||
<div class="lia-sug" id="liaSug"></div>
|
||||
</div>
|
||||
<div class="lia-in">
|
||||
<input type="text" id="liaInput" placeholder="¿Cómo…?" autocomplete="off">
|
||||
<button id="liaSend" onclick="liaEnviar()"><i class="fas fa-paper-plane"></i></button>
|
||||
<div class="ai-body" id="aiBody">
|
||||
<div class="ai-quick" id="aiQuick"></div>
|
||||
<div class="ai-messages" id="aiMessages"></div>
|
||||
<div class="ai-footer">
|
||||
<input type="text" id="aiInput" placeholder="Pregunta cómo hacer algo…" maxlength="400"
|
||||
onkeydown="if(event.key==='Enter')enviarIA()">
|
||||
<button class="btn-send" onclick="enviarIA()"><i class="fas fa-paper-plane"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -338,36 +383,60 @@ function esc(s) {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
/* ── Asistente LIA sobre la documentación ── */
|
||||
const LIA_API = '<?= BASE_URL ?>modules/soporte/api/ai_docs.php';
|
||||
let _liaHist = [];
|
||||
/* ── LIA: asistente sobre la documentación ──
|
||||
Mismo comportamiento y nombres que el panel del dashboard del turnero. */
|
||||
const AI_API = '<?= BASE_URL ?>modules/soporte/api/ai_docs.php';
|
||||
let _liaHistorial = [];
|
||||
|
||||
function liaToggle() {
|
||||
const p = document.getElementById('liaPanel');
|
||||
p.classList.toggle('abierto');
|
||||
if (p.classList.contains('abierto')) document.getElementById('liaInput').focus();
|
||||
function toggleAI() {
|
||||
const panel = document.getElementById('aiPanel');
|
||||
const fab = document.getElementById('btnAI');
|
||||
const abre = !panel.classList.contains('open');
|
||||
panel.classList.toggle('open', abre);
|
||||
panel.classList.remove('minimized');
|
||||
fab.classList.toggle('hidden', abre);
|
||||
if (abre) {
|
||||
if (!document.getElementById('aiMessages').children.length) saludarIA();
|
||||
document.getElementById('aiInput').focus();
|
||||
}
|
||||
}
|
||||
document.getElementById('liaFab').addEventListener('click', liaToggle);
|
||||
|
||||
// Sugerencias tomadas de los documentos que este usuario realmente puede ver
|
||||
(function () {
|
||||
const sug = document.getElementById('liaSug');
|
||||
if (!DOCS.length) return;
|
||||
sug.innerHTML = DOCS.slice(0, 3)
|
||||
.map(d => `<button onclick="liaPreguntar('¿Qué explica ${esc(d.t)}?')">${esc(d.t)}</button>`)
|
||||
function minimizarIA(e) {
|
||||
if (e) e.stopPropagation();
|
||||
const panel = document.getElementById('aiPanel');
|
||||
panel.classList.toggle('minimized');
|
||||
const icono = document.getElementById('iconMin');
|
||||
icono.className = panel.classList.contains('minimized') ? 'fas fa-chevron-up' : 'fas fa-minus';
|
||||
}
|
||||
|
||||
function saludarIA() {
|
||||
agregarMsg('bot', 'Hola, soy LIA. Pregúnteme cómo hacer algo en el sistema y le respondo con la documentación.');
|
||||
// Atajos armados con los documentos que este usuario realmente puede ver
|
||||
const quick = document.getElementById('aiQuick');
|
||||
quick.innerHTML = DOCS.slice(0, 4)
|
||||
.map(d => `<button class="ai-chip" onclick="enviarRapido('¿Qué explica ${esc(d.t)}?')">${esc(d.t)}</button>`)
|
||||
.join('');
|
||||
})();
|
||||
}
|
||||
|
||||
function liaPreguntar(texto) {
|
||||
document.getElementById('liaInput').value = texto;
|
||||
liaEnviar();
|
||||
function enviarRapido(texto) {
|
||||
document.getElementById('aiInput').value = texto;
|
||||
enviarIA();
|
||||
}
|
||||
|
||||
function agregarMsg(tipo, html) {
|
||||
const msgs = document.getElementById('aiMessages');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'ai-msg ' + tipo;
|
||||
div.innerHTML = html;
|
||||
msgs.appendChild(div);
|
||||
msgs.scrollTop = msgs.scrollHeight;
|
||||
return div;
|
||||
}
|
||||
|
||||
/* Formato mínimo de la respuesta: negritas, código, listas y saltos. */
|
||||
function liaFmt(md) {
|
||||
function mdToHtml(md) {
|
||||
let h = esc(md);
|
||||
h = h.replace(/```[\s\S]*?```/g, m => '<pre style="white-space:pre-wrap;font-size:.75rem">'
|
||||
+ m.replace(/```\w*\n?/g, '') + '</pre>');
|
||||
h = h.replace(/```[\s\S]*?```/g, m => '<pre>' + m.replace(/```\w*\n?/g, '') + '</pre>');
|
||||
h = h.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
h = h.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
h = h.replace(/^\s*[-*]\s+(.*)$/gm, '<li>$1</li>');
|
||||
@@ -375,57 +444,51 @@ function liaFmt(md) {
|
||||
return h.replace(/\n{2,}/g, '<br><br>').replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
async function liaEnviar() {
|
||||
const input = document.getElementById('liaInput');
|
||||
const btn = document.getElementById('liaSend');
|
||||
const msgs = document.getElementById('liaMsgs');
|
||||
const q = input.value.trim();
|
||||
if (!q) return;
|
||||
async function enviarIA() {
|
||||
const input = document.getElementById('aiInput');
|
||||
const pregunta = input.value.trim();
|
||||
if (!pregunta) return;
|
||||
|
||||
document.getElementById('liaSug')?.remove();
|
||||
msgs.insertAdjacentHTML('beforeend', `<div class="lia-msg user">${esc(q)}</div>`);
|
||||
|
||||
const pensando = document.createElement('div');
|
||||
pensando.className = 'lia-msg bot';
|
||||
pensando.textContent = 'Buscando en la documentación…';
|
||||
msgs.appendChild(pensando);
|
||||
msgs.scrollTop = msgs.scrollHeight;
|
||||
document.getElementById('aiQuick').innerHTML = '';
|
||||
agregarMsg('user', esc(pregunta));
|
||||
|
||||
const typing = agregarMsg('bot typing', '');
|
||||
input.value = '';
|
||||
input.disabled = btn.disabled = true;
|
||||
input.disabled = true;
|
||||
|
||||
try {
|
||||
const r = await fetch(LIA_API, {
|
||||
const res = await fetch(AI_API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pregunta: q, historial: _liaHist }),
|
||||
body: JSON.stringify({ pregunta, historial: _liaHistorial }),
|
||||
});
|
||||
const d = await r.json();
|
||||
const d = await res.json();
|
||||
typing.className = 'ai-msg bot';
|
||||
|
||||
if (d.ok) {
|
||||
pensando.innerHTML = liaFmt(d.respuesta);
|
||||
typing.innerHTML = mdToHtml(d.respuesta);
|
||||
if (d.fuentes && d.fuentes.length) {
|
||||
msgs.insertAdjacentHTML('beforeend',
|
||||
`<div class="lia-fuentes">Según: ${d.fuentes.map(esc).join(' · ')}</div>`);
|
||||
const f = document.createElement('div');
|
||||
f.className = 'ai-fuentes';
|
||||
f.textContent = 'Según: ' + d.fuentes.join(' · ');
|
||||
document.getElementById('aiMessages').appendChild(f);
|
||||
}
|
||||
_liaHist.push({ rol: 'user', texto: q }, { rol: 'model', texto: d.respuesta });
|
||||
if (_liaHist.length > 6) _liaHist = _liaHist.slice(-6);
|
||||
_liaHistorial.push({ rol: 'user', texto: pregunta }, { rol: 'model', texto: d.respuesta });
|
||||
if (_liaHistorial.length > 6) _liaHistorial = _liaHistorial.slice(-6);
|
||||
} else {
|
||||
pensando.textContent = d.error || 'No pude responder.';
|
||||
typing.textContent = d.error || 'No pude responder.';
|
||||
}
|
||||
} catch (_) {
|
||||
pensando.textContent = 'Error de conexión.';
|
||||
typing.className = 'ai-msg bot';
|
||||
typing.textContent = 'Error de conexión';
|
||||
} finally {
|
||||
input.disabled = btn.disabled = false;
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
const msgs = document.getElementById('aiMessages');
|
||||
msgs.scrollTop = msgs.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('liaInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') liaEnviar();
|
||||
});
|
||||
|
||||
const inputQ = document.getElementById('q');
|
||||
let _t = null;
|
||||
inputQ.addEventListener('input', () => {
|
||||
|
||||
Reference in New Issue
Block a user