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>
506 lines
24 KiB
PHP
506 lines
24 KiB
PHP
<?php
|
||
/**
|
||
* modules/soporte/views/documentacion.php
|
||
* Visor de la documentación del proyecto.
|
||
*
|
||
* ?s=<seccion>&d=<documento> documento puntual
|
||
* ?s=<seccion> portada de la sección
|
||
* (sin parámetros) portada general
|
||
*/
|
||
require_once APP_ROOT . '/config/config.php';
|
||
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
||
|
||
require_once __DIR__ . '/../Markdown.php';
|
||
require_once __DIR__ . '/../DocIndex.php';
|
||
require_once __DIR__ . '/../Generadores.php';
|
||
|
||
$arbol = DocIndex::arbol();
|
||
$seccion = preg_replace('/[^a-z0-9-]/', '', $_GET['s'] ?? '');
|
||
$docSlug = preg_replace('/[^a-z0-9-]/', '', $_GET['d'] ?? '');
|
||
|
||
$archivo = ($seccion && $docSlug) ? DocIndex::resolver($seccion, $docSlug) : null;
|
||
$titulo = 'Documentación';
|
||
$cuerpo = '';
|
||
$toc = [];
|
||
|
||
if ($archivo) {
|
||
[, $crudo] = DocIndex::leer($archivo); // descarta la cabecera de permisos
|
||
$md = Generadores::expandir($crudo);
|
||
$cuerpo = Markdown::render($md);
|
||
$toc = Markdown::indice($md);
|
||
foreach ($arbol[$seccion]['docs'] ?? [] as $d) {
|
||
if ($d['slug'] === $docSlug) { $titulo = $d['titulo']; break; }
|
||
}
|
||
} elseif ($seccion && isset($arbol[$seccion])) {
|
||
$titulo = $arbol[$seccion]['titulo'];
|
||
}
|
||
|
||
Layout::open('Soporte · Documentación', 'fas fa-life-ring');
|
||
?>
|
||
<style>
|
||
.doc-wrap { display:flex; gap:0; align-items:flex-start; min-height:calc(100vh - 60px); background:#fff; }
|
||
|
||
/* ── Índice lateral ── */
|
||
.doc-nav { width:280px; flex-shrink:0; border-right:1px solid #e2e8f0; background:#f8fafc;
|
||
align-self:stretch; padding:18px 0 60px; position:sticky; top:0; max-height:100vh; overflow-y:auto; }
|
||
.doc-nav .buscador { padding:0 16px 14px; }
|
||
.doc-nav .buscador input { width:100%; font-size:.84rem; padding:7px 11px; border:1px solid #cbd5e1;
|
||
border-radius:8px; background:#fff; }
|
||
.doc-nav .buscador input:focus { outline:none; border-color:#2563eb; box-shadow:0 0 0 3px rgba(37,99,235,.12); }
|
||
.doc-nav-sec { padding:10px 16px 4px; font-size:.68rem; font-weight:800; text-transform:uppercase;
|
||
letter-spacing:.08em; color:#64748b; display:flex; align-items:center; gap:6px; }
|
||
.doc-nav a { display:block; padding:5px 16px 5px 30px; font-size:.83rem; color:#334155;
|
||
text-decoration:none; border-left:2px solid transparent; }
|
||
.doc-nav a:hover { background:#eef2f7; color:#1e293b; }
|
||
.doc-nav a.activo { background:#e0edff; color:#1d4ed8; font-weight:600; border-left-color:#2563eb; }
|
||
|
||
/* ── Resultados de búsqueda ── */
|
||
#resultados { padding:0 16px; }
|
||
#resultados .r { display:block; padding:8px 10px; border-radius:8px; text-decoration:none;
|
||
margin-bottom:4px; background:#fff; border:1px solid #e2e8f0; }
|
||
#resultados .r:hover { border-color:#2563eb; }
|
||
#resultados .r-t { font-size:.83rem; font-weight:600; color:#1e293b; }
|
||
#resultados .r-c { font-size:.68rem; color:#64748b; text-transform:uppercase; letter-spacing:.05em; }
|
||
#resultados .r-x { font-size:.75rem; color:#475569; margin-top:2px; }
|
||
#resultados .vacio { font-size:.8rem; color:#94a3b8; padding:8px 4px; }
|
||
|
||
/* ── Contenido ── */
|
||
.doc-main { flex:1; min-width:0; display:flex; gap:0; }
|
||
.doc-body { flex:1; min-width:0; padding:26px 40px 80px; max-width:900px; }
|
||
.doc-ruta { font-size:.73rem; color:#94a3b8; margin-bottom:14px; }
|
||
.doc-ruta a { color:#64748b; text-decoration:none; }
|
||
.doc-ruta a:hover { text-decoration:underline; }
|
||
|
||
.doc-body h1 { font-size:1.65rem; font-weight:800; color:#0f172a; margin:0 0 18px;
|
||
padding-bottom:12px; border-bottom:1px solid #e2e8f0; }
|
||
.doc-body h2 { font-size:1.2rem; font-weight:700; color:#1e293b; margin:32px 0 12px;
|
||
padding-top:6px; scroll-margin-top:20px; }
|
||
.doc-body h3 { font-size:1rem; font-weight:700; color:#334155; margin:24px 0 9px; scroll-margin-top:20px; }
|
||
.doc-body h4 { font-size:.9rem; font-weight:700; color:#475569; margin:18px 0 7px; }
|
||
.doc-body p { font-size:.9rem; line-height:1.7; color:#334155; margin:0 0 13px; }
|
||
.doc-body ul, .doc-body ol { font-size:.9rem; line-height:1.7; color:#334155; margin:0 0 13px; padding-left:24px; }
|
||
.doc-body li { margin-bottom:4px; }
|
||
.doc-body li > ul, .doc-body li > ol { margin:4px 0 2px; }
|
||
.doc-body a { color:#2563eb; }
|
||
.doc-body strong { color:#0f172a; }
|
||
.doc-body code { background:#f1f5f9; color:#be185d; padding:1px 5px; border-radius:4px;
|
||
font-size:.83em; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
|
||
.doc-body pre { background:#0f172a; color:#e2e8f0; padding:14px 16px; border-radius:10px;
|
||
overflow-x:auto; margin:0 0 15px; }
|
||
.doc-body pre code { background:none; color:inherit; padding:0; font-size:.8rem; line-height:1.6; }
|
||
.doc-body blockquote { border-left:3px solid #f59e0b; background:#fffbeb; margin:0 0 15px;
|
||
padding:10px 16px; border-radius:0 8px 8px 0; }
|
||
.doc-body blockquote p { margin:0; color:#78350f; font-size:.86rem; }
|
||
.doc-body hr { border:none; border-top:1px solid #e2e8f0; margin:26px 0; }
|
||
.tabla-scroll { overflow-x:auto; margin:0 0 16px; }
|
||
.doc-body table { width:100%; border-collapse:collapse; font-size:.82rem; }
|
||
.doc-body thead th { background:#f8fafc; text-align:left; font-weight:700; color:#475569;
|
||
padding:8px 11px; border-bottom:2px solid #e2e8f0; white-space:nowrap; }
|
||
.doc-body tbody td { padding:7px 11px; border-bottom:1px solid #f1f5f9; color:#334155; vertical-align:top; }
|
||
.doc-body tbody tr:hover { background:#f8fafc; }
|
||
|
||
/* ── Tabla de contenidos ── */
|
||
.doc-toc { width:210px; flex-shrink:0; padding:32px 20px 60px; position:sticky; top:0;
|
||
max-height:100vh; overflow-y:auto; }
|
||
.doc-toc-t { font-size:.66rem; font-weight:800; text-transform:uppercase; letter-spacing:.08em;
|
||
color:#94a3b8; margin-bottom:9px; }
|
||
.doc-toc a { display:block; font-size:.76rem; color:#64748b; text-decoration:none;
|
||
padding:3px 0 3px 9px; border-left:2px solid #e2e8f0; line-height:1.4; }
|
||
.doc-toc a:hover { color:#2563eb; border-left-color:#93c5fd; }
|
||
.doc-toc a.n3 { padding-left:20px; font-size:.73rem; }
|
||
|
||
/* ── Portadas ── */
|
||
.doc-hero { margin-bottom:26px; }
|
||
.doc-hero h1 { font-size:1.7rem; font-weight:800; color:#0f172a; margin:0 0 8px; }
|
||
.doc-hero p { font-size:.92rem; color:#64748b; margin:0; }
|
||
.tarjetas { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:14px; }
|
||
.tarjeta { display:block; padding:18px; border:1px solid #e2e8f0; border-radius:12px;
|
||
text-decoration:none; background:#fff; transition:border-color .15s, transform .15s; }
|
||
.tarjeta:hover { border-color:#2563eb; transform:translateY(-2px); }
|
||
.tarjeta i { font-size:1.3rem; color:#2563eb; }
|
||
.tarjeta .t { font-size:1rem; font-weight:700; color:#1e293b; margin:9px 0 5px; }
|
||
.tarjeta .d { font-size:.82rem; color:#64748b; line-height:1.5; }
|
||
.tarjeta .n { font-size:.72rem; color:#94a3b8; margin-top:9px; }
|
||
|
||
.lista-docs { list-style:none; padding:0; margin:0; }
|
||
.lista-docs li { border-bottom:1px solid #f1f5f9; }
|
||
.lista-docs a { display:block; padding:12px 4px; text-decoration:none; color:#1e293b; font-size:.92rem; }
|
||
.lista-docs a:hover { color:#2563eb; }
|
||
|
||
@media (max-width:1100px) { .doc-toc { display:none; } }
|
||
@media (max-width:820px) {
|
||
.doc-nav { position:static; width:100%; max-height:none; border-right:none;
|
||
border-bottom:1px solid #e2e8f0; }
|
||
.doc-wrap { flex-direction:column; }
|
||
.doc-body { padding:20px 18px 60px; }
|
||
}
|
||
@media print {
|
||
.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; }
|
||
}
|
||
|
||
/* ── 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">
|
||
<!-- ── Índice lateral ── -->
|
||
<nav class="doc-nav">
|
||
<div class="buscador">
|
||
<input type="search" id="q" placeholder="Buscar en la documentación…" autocomplete="off">
|
||
</div>
|
||
<div id="resultados" style="display:none"></div>
|
||
<div id="arbol">
|
||
<?php foreach ($arbol as $sec => $cfg): ?>
|
||
<div class="doc-nav-sec">
|
||
<i class="<?= htmlspecialchars($cfg['icono']) ?>"></i>
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($sec) ?>"
|
||
style="padding:0;border:none;color:inherit;font:inherit;letter-spacing:inherit">
|
||
<?= htmlspecialchars($cfg['titulo']) ?>
|
||
</a>
|
||
</div>
|
||
<?php foreach ($cfg['docs'] as $d): ?>
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($sec) ?>&d=<?= urlencode($d['slug']) ?>"
|
||
class="<?= ($sec === $seccion && $d['slug'] === $docSlug) ? 'activo' : '' ?>">
|
||
<?= htmlspecialchars($d['titulo']) ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="doc-main">
|
||
<div class="doc-body">
|
||
<?php if ($archivo): ?>
|
||
<div class="doc-ruta">
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion">Documentación</a>
|
||
›
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($seccion) ?>">
|
||
<?= htmlspecialchars($arbol[$seccion]['titulo'] ?? $seccion) ?>
|
||
</a>
|
||
› <?= htmlspecialchars($titulo) ?>
|
||
</div>
|
||
<?= $cuerpo ?>
|
||
|
||
<?php elseif ($seccion && isset($arbol[$seccion])): ?>
|
||
<div class="doc-ruta">
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion">Documentación</a>
|
||
› <?= htmlspecialchars($arbol[$seccion]['titulo']) ?>
|
||
</div>
|
||
<div class="doc-hero">
|
||
<h1><?= htmlspecialchars($arbol[$seccion]['titulo']) ?></h1>
|
||
<p><?= htmlspecialchars($arbol[$seccion]['resumen']) ?></p>
|
||
</div>
|
||
<?php if ($arbol[$seccion]['docs']): ?>
|
||
<ul class="lista-docs">
|
||
<?php foreach ($arbol[$seccion]['docs'] as $d): ?>
|
||
<li><a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($seccion) ?>&d=<?= urlencode($d['slug']) ?>">
|
||
<?= htmlspecialchars($d['titulo']) ?>
|
||
</a></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php else: ?>
|
||
<p class="text-muted">Esta sección aún no tiene documentos.</p>
|
||
<?php endif; ?>
|
||
|
||
<?php else: ?>
|
||
<div class="doc-hero">
|
||
<h1>Documentación del sistema</h1>
|
||
<p>Todo sobre cómo funciona este ERP: cómo se usa, cómo está construido y qué hacer cuando algo falla.</p>
|
||
</div>
|
||
<div class="tarjetas">
|
||
<?php foreach ($arbol as $sec => $cfg): ?>
|
||
<a class="tarjeta" href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($sec) ?>">
|
||
<i class="<?= htmlspecialchars($cfg['icono']) ?>"></i>
|
||
<div class="t"><?= htmlspecialchars($cfg['titulo']) ?></div>
|
||
<div class="d"><?= htmlspecialchars($cfg['resumen']) ?></div>
|
||
<div class="n"><?= count($cfg['docs']) ?> documento<?= count($cfg['docs']) === 1 ? '' : 's' ?></div>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if ($toc): ?>
|
||
<aside class="doc-toc">
|
||
<div class="doc-toc-t">En esta página</div>
|
||
<?php foreach ($toc as $t): ?>
|
||
<a href="#<?= htmlspecialchars($t['slug']) ?>" class="<?= $t['nivel'] === 3 ? 'n3' : '' ?>">
|
||
<?= htmlspecialchars($t['texto']) ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</aside>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 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>
|
||
<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="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>
|
||
|
||
<script>
|
||
/* ── Buscador: índice completo servido con la página ── */
|
||
const DOCS = <?= json_encode(DocIndex::indiceBusqueda(), JSON_UNESCAPED_UNICODE) ?>;
|
||
const BASEQ = '<?= BASE_URL ?>erp.php?m=soporte&v=documentacion';
|
||
|
||
const norm = s => (s || '').toLowerCase()
|
||
.normalize('NFD').replace(/[̀-ͯ]/g, '');
|
||
|
||
function buscar(termino) {
|
||
const q = norm(termino).trim();
|
||
const cajaR = document.getElementById('resultados');
|
||
const cajaA = document.getElementById('arbol');
|
||
if (q.length < 2) { cajaR.style.display = 'none'; cajaA.style.display = ''; return; }
|
||
|
||
const palabras = q.split(/\s+/);
|
||
const hits = [];
|
||
DOCS.forEach(d => {
|
||
const heno = norm(d.t + ' ' + d.c + ' ' + d.x);
|
||
if (!palabras.every(p => heno.includes(p))) return;
|
||
// El título pesa más que el cuerpo
|
||
const enTitulo = palabras.every(p => norm(d.t).includes(p));
|
||
const pos = heno.indexOf(palabras[0]);
|
||
hits.push({ d, score: (enTitulo ? 0 : 1000) + pos, pos });
|
||
});
|
||
hits.sort((a, b) => a.score - b.score);
|
||
|
||
cajaA.style.display = 'none';
|
||
cajaR.style.display = '';
|
||
if (!hits.length) {
|
||
cajaR.innerHTML = '<div class="vacio">Sin resultados para «' + esc(termino) + '»</div>';
|
||
return;
|
||
}
|
||
cajaR.innerHTML = hits.slice(0, 20).map(h => {
|
||
const ini = Math.max(0, h.pos - 40);
|
||
const frag = h.d.x.substr(ini, 120).trim();
|
||
return `<a class="r" href="${BASEQ}&s=${encodeURIComponent(h.d.s)}&d=${encodeURIComponent(h.d.u)}">
|
||
<div class="r-c">${esc(h.d.c)}</div>
|
||
<div class="r-t">${esc(h.d.t)}</div>
|
||
<div class="r-x">${ini > 0 ? '…' : ''}${esc(frag)}…</div>
|
||
</a>`;
|
||
}).join('');
|
||
}
|
||
|
||
function esc(s) {
|
||
const d = document.createElement('div');
|
||
d.textContent = s == null ? '' : String(s);
|
||
return d.innerHTML;
|
||
}
|
||
|
||
/* ── 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 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();
|
||
}
|
||
}
|
||
|
||
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 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 mdToHtml(md) {
|
||
let h = esc(md);
|
||
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>');
|
||
h = h.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
|
||
return h.replace(/\n{2,}/g, '<br><br>').replace(/\n/g, '<br>');
|
||
}
|
||
|
||
async function enviarIA() {
|
||
const input = document.getElementById('aiInput');
|
||
const pregunta = input.value.trim();
|
||
if (!pregunta) return;
|
||
|
||
document.getElementById('aiQuick').innerHTML = '';
|
||
agregarMsg('user', esc(pregunta));
|
||
|
||
const typing = agregarMsg('bot typing', '');
|
||
input.value = '';
|
||
input.disabled = true;
|
||
|
||
try {
|
||
const res = await fetch(AI_API, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ pregunta, historial: _liaHistorial }),
|
||
});
|
||
const d = await res.json();
|
||
typing.className = 'ai-msg bot';
|
||
|
||
if (d.ok) {
|
||
typing.innerHTML = mdToHtml(d.respuesta);
|
||
if (d.fuentes && d.fuentes.length) {
|
||
const f = document.createElement('div');
|
||
f.className = 'ai-fuentes';
|
||
f.textContent = 'Según: ' + d.fuentes.join(' · ');
|
||
document.getElementById('aiMessages').appendChild(f);
|
||
}
|
||
_liaHistorial.push({ rol: 'user', texto: pregunta }, { rol: 'model', texto: d.respuesta });
|
||
if (_liaHistorial.length > 6) _liaHistorial = _liaHistorial.slice(-6);
|
||
} else {
|
||
typing.textContent = d.error || 'No pude responder.';
|
||
}
|
||
} catch (_) {
|
||
typing.className = 'ai-msg bot';
|
||
typing.textContent = 'Error de conexión';
|
||
} finally {
|
||
input.disabled = false;
|
||
input.focus();
|
||
const msgs = document.getElementById('aiMessages');
|
||
msgs.scrollTop = msgs.scrollHeight;
|
||
}
|
||
}
|
||
|
||
const inputQ = document.getElementById('q');
|
||
let _t = null;
|
||
inputQ.addEventListener('input', () => {
|
||
clearTimeout(_t);
|
||
_t = setTimeout(() => buscar(inputQ.value), 120);
|
||
});
|
||
// Atajo: "/" enfoca el buscador
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === '/' && document.activeElement !== inputQ) { e.preventDefault(); inputQ.focus(); }
|
||
if (e.key === 'Escape' && document.activeElement === inputQ) { inputQ.value = ''; buscar(''); inputQ.blur(); }
|
||
});
|
||
</script>
|
||
|
||
<?php Layout::close(); ?>
|