DocIndex::contextoIA() selecciona los documentos relevantes a la pregunta y devuelve su texto. Reutiliza el filtrado por rol ya existente: al contexto que se envía al modelo solo entra lo que ese usuario podría leer por su cuenta en el módulo Soporte, así el asistente no puede revelar contenido restringido. Verificado: un recepcionista preguntando por permisos recibe solo el manual básico, sin el SQL ni los detalles internos que sí recibe un administrador. - services/GeminiService.php concentra la llamada a la API y la contabilidad del presupuesto, que antes vivía dentro de ai_chat.php y ahora comparten los dos asistentes. - La LIA del turnero suma la documentación a su contexto operativo, así responde tanto "cuánto facturamos hoy" como "cómo marco un paciente ausente". - modules/soporte/api/ai_docs.php es el asistente de la documentación: no accede a datos de pacientes ni de la operación, solo a los documentos visibles. A diferencia del anterior no exige acceso al turnero, así que lo puede usar cualquier usuario autenticado desde la página de documentación. - La respuesta cita de qué documentos salió. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
443 lines
22 KiB
PHP
443 lines
22 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, .lia-fab, .lia-panel { 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; }
|
||
</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>
|
||
|
||
<!-- ── 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>
|
||
</div>
|
||
<button onclick="liaToggle()" title="Cerrar"><i class="fas fa-times"></i></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>
|
||
</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;
|
||
}
|
||
|
||
/* ── Asistente LIA sobre la documentación ── */
|
||
const LIA_API = '<?= BASE_URL ?>modules/soporte/api/ai_docs.php';
|
||
let _liaHist = [];
|
||
|
||
function liaToggle() {
|
||
const p = document.getElementById('liaPanel');
|
||
p.classList.toggle('abierto');
|
||
if (p.classList.contains('abierto')) document.getElementById('liaInput').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>`)
|
||
.join('');
|
||
})();
|
||
|
||
function liaPreguntar(texto) {
|
||
document.getElementById('liaInput').value = texto;
|
||
liaEnviar();
|
||
}
|
||
|
||
/* Formato mínimo de la respuesta: negritas, código, listas y saltos. */
|
||
function liaFmt(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(/`([^`]+)`/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 liaEnviar() {
|
||
const input = document.getElementById('liaInput');
|
||
const btn = document.getElementById('liaSend');
|
||
const msgs = document.getElementById('liaMsgs');
|
||
const q = input.value.trim();
|
||
if (!q) 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;
|
||
|
||
input.value = '';
|
||
input.disabled = btn.disabled = true;
|
||
|
||
try {
|
||
const r = await fetch(LIA_API, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ pregunta: q, historial: _liaHist }),
|
||
});
|
||
const d = await r.json();
|
||
|
||
if (d.ok) {
|
||
pensando.innerHTML = liaFmt(d.respuesta);
|
||
if (d.fuentes && d.fuentes.length) {
|
||
msgs.insertAdjacentHTML('beforeend',
|
||
`<div class="lia-fuentes">Según: ${d.fuentes.map(esc).join(' · ')}</div>`);
|
||
}
|
||
_liaHist.push({ rol: 'user', texto: q }, { rol: 'model', texto: d.respuesta });
|
||
if (_liaHist.length > 6) _liaHist = _liaHist.slice(-6);
|
||
} else {
|
||
pensando.textContent = d.error || 'No pude responder.';
|
||
}
|
||
} catch (_) {
|
||
pensando.textContent = 'Error de conexión.';
|
||
} finally {
|
||
input.disabled = btn.disabled = false;
|
||
input.focus();
|
||
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', () => {
|
||
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(); ?>
|