Files
Laucha1312 cc049c6cb6 3
2026-06-04 15:20:26 -03:00

381 lines
12 KiB
PHP

{{-- OnAPB Genius Agent Chat Bubble --}}
<style>
#genius-btn {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 1050;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #0d6efd, #6610f2);
color: #fff;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,.25);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
cursor: pointer;
transition: transform .15s;
}
#genius-btn:hover { transform: scale(1.08); }
#genius-panel {
position: fixed;
bottom: 5rem;
right: 1.5rem;
z-index: 1049;
width: 360px;
max-width: calc(100vw - 2rem);
background: #fff;
border-radius: 1rem;
box-shadow: 0 8px 32px rgba(0,0,0,.18);
display: flex;
flex-direction: column;
overflow: hidden;
transition: opacity .2s, transform .2s;
transform-origin: bottom right;
}
#genius-panel.genius-hidden {
opacity: 0;
pointer-events: none;
transform: scale(.92);
}
/* Mobile: panel casi pantalla completa para que el teclado no tape el input. */
@media (max-width: 576px) {
#genius-panel {
left: .5rem;
right: .5rem;
bottom: .5rem;
top: auto;
width: auto;
max-width: none;
border-radius: .85rem;
max-height: calc(100dvh - 1rem);
}
#genius-messages { max-height: none; flex: 1 1 auto; }
}
#genius-header {
background: linear-gradient(135deg, #0d6efd, #6610f2);
color: #fff;
padding: .75rem 1rem;
display: flex;
align-items: center;
gap: .5rem;
}
#genius-header .genius-title { font-weight: 600; font-size: .95rem; flex: 1; }
#genius-header button { background: none; border: none; color: #fff; font-size: 1rem; line-height: 1; padding: 0; cursor: pointer; opacity: .8; }
#genius-header button:hover { opacity: 1; }
#genius-messages {
flex: 1;
overflow-y: auto;
padding: .75rem 1rem;
max-height: 340px;
min-height: 120px;
display: flex;
flex-direction: column;
gap: .5rem;
}
.genius-msg {
padding: .5rem .75rem;
border-radius: .75rem;
font-size: .875rem;
line-height: 1.45;
max-width: 88%;
word-break: break-word;
}
.genius-msg.user {
align-self: flex-end;
background: #0d6efd;
color: #fff;
border-bottom-right-radius: .2rem;
}
.genius-msg.assistant {
align-self: flex-start;
background: #f1f3f5;
color: #212529;
border-bottom-left-radius: .2rem;
}
.genius-msg.error { background: #fff3cd; color: #856404; }
.genius-msg strong { font-weight: 600; }
.genius-msg em { font-style: italic; }
.genius-msg code {
background: rgba(0,0,0,.08);
padding: .1rem .3rem;
border-radius: .25rem;
font-size: .85em;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.genius-msg.user code { background: rgba(255,255,255,.22); }
#genius-typing {
align-self: flex-start;
padding: .4rem .75rem;
background: #f1f3f5;
border-radius: .75rem;
font-size: .8rem;
color: #6c757d;
display: none;
}
#genius-form {
border-top: 1px solid #dee2e6;
padding: .6rem .75rem;
display: flex;
gap: .4rem;
background: #fff;
}
#genius-input {
flex: 1;
border: 1px solid #dee2e6;
border-radius: .5rem;
padding: .4rem .6rem;
font-size: .875rem;
outline: none;
resize: none;
max-height: 80px;
overflow-y: auto;
}
#genius-input:focus { border-color: #0d6efd; }
#genius-send {
background: #0d6efd;
color: #fff;
border: none;
border-radius: .5rem;
padding: .4rem .7rem;
font-size: 1rem;
cursor: pointer;
transition: background .15s;
align-self: flex-end;
}
#genius-send:hover { background: #0b5ed7; }
#genius-send:disabled { background: #adb5bd; cursor: not-allowed; }
</style>
<button id="genius-btn" title="OnAPB Genius" aria-label="Abrir asistente">
<i class="bi bi-stars"></i>
</button>
<div id="genius-panel" class="genius-hidden">
<div id="genius-header">
<i class="bi bi-stars"></i>
<span class="genius-title">OnAPB Genius</span>
<button id="genius-close" aria-label="Cerrar"><i class="bi bi-x-lg"></i></button>
</div>
<div id="genius-messages">
<div class="genius-msg assistant">
¡Hola! Soy OnAPB Genius. ¿En qué puedo ayudarte hoy?
</div>
<div id="genius-typing">Escribiendo<span id="genius-dots">...</span></div>
</div>
<form id="genius-form">
@csrf
<textarea id="genius-input" placeholder="Escribí tu consulta..." rows="1" maxlength="1000"></textarea>
<button type="submit" id="genius-send"><i class="bi bi-send-fill"></i></button>
</form>
</div>
<script>
(function () {
const btn = document.getElementById('genius-btn');
const panel = document.getElementById('genius-panel');
const closeBtn = document.getElementById('genius-close');
const form = document.getElementById('genius-form');
const input = document.getElementById('genius-input');
const sendBtn = document.getElementById('genius-send');
const msgs = document.getElementById('genius-messages');
const typing = document.getElementById('genius-typing');
const STORAGE_KEY = 'onapb_genius_chat_v1';
let threadId = null;
let isOpen = false;
function loadState() {
try {
const raw = sessionStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : null;
} catch (e) {
return null;
}
}
function saveState() {
try {
const messages = Array.from(msgs.querySelectorAll('.genius-msg')).map(el => ({
role: el.classList.contains('user') ? 'user'
: el.classList.contains('error') ? 'error'
: 'assistant',
text: el.dataset.text ?? el.textContent
}));
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
messages, threadId, isOpen
}));
} catch (e) {}
}
function escapeHtml(s) {
return s.replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}
function renderMarkdown(text) {
let s = escapeHtml(text);
const codes = [];
s = s.replace(/`([^`\n]+?)`/g, (_, code) => {
codes.push(code);
return `C${codes.length - 1}`;
});
s = s.replace(/\*\*\*([^*\n]+?)\*\*\*/g, '<strong><em>$1</em></strong>');
s = s.replace(/\*\*([^*\n]+?)\*\*/g, '<strong>$1</strong>');
s = s.replace(/(^|[^*\w])\*([^*\n]+?)\*(?!\*)/g, '$1<em>$2</em>');
s = s.replace(/(^|[^_\w])_([^_\n]+?)_(?!\w)/g, '$1<em>$2</em>');
s = s.replace(/C(\d+)/g, (_, i) => `<code>${codes[i]}</code>`);
s = s.replace(/\n/g, '<br>');
return s;
}
function renderMessage(role, text) {
const div = document.createElement('div');
div.className = 'genius-msg ' + role;
div.dataset.text = text;
if (role === 'assistant') {
div.innerHTML = renderMarkdown(text);
} else {
div.textContent = text;
}
msgs.insertBefore(div, typing);
}
function rehydrate() {
const saved = loadState();
if (!saved) return;
if (Array.isArray(saved.messages) && saved.messages.length > 0) {
msgs.querySelectorAll('.genius-msg').forEach(el => el.remove());
saved.messages.forEach(m => renderMessage(m.role, m.text));
msgs.scrollTop = msgs.scrollHeight;
}
if (saved.threadId) threadId = saved.threadId;
if (saved.isOpen) {
isOpen = true;
panel.classList.remove('genius-hidden');
}
}
function togglePanel() {
isOpen = !isOpen;
panel.classList.toggle('genius-hidden', !isOpen);
if (isOpen) input.focus();
adjustForKeyboard();
saveState();
}
// En mobile el teclado virtual reduce el visualViewport pero el layout viewport
// queda igual, así que un fixed con bottom queda tapado. Compensamos manualmente.
function adjustForKeyboard() {
if (!window.visualViewport || window.innerWidth > 576) {
panel.style.bottom = '';
return;
}
if (!isOpen) { panel.style.bottom = ''; return; }
const vv = window.visualViewport;
const keyboardHeight = window.innerHeight - vv.height - vv.offsetTop;
panel.style.bottom = (keyboardHeight > 80 ? keyboardHeight + 8 : 8) + 'px';
}
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', adjustForKeyboard);
window.visualViewport.addEventListener('scroll', adjustForKeyboard);
}
window.addEventListener('resize', adjustForKeyboard);
btn.addEventListener('click', togglePanel);
closeBtn.addEventListener('click', togglePanel);
// Auto-resize textarea
input.addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 80) + 'px';
});
// Submit on Enter (Shift+Enter = nueva línea)
input.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
form.dispatchEvent(new Event('submit'));
}
});
function appendMsg(role, text) {
typing.style.display = 'none';
renderMessage(role, text);
msgs.scrollTop = msgs.scrollHeight;
saveState();
}
function setLoading(loading) {
sendBtn.disabled = loading;
input.disabled = loading;
typing.style.display = loading ? 'block' : 'none';
if (loading) msgs.scrollTop = msgs.scrollHeight;
}
form.addEventListener('submit', async function (e) {
e.preventDefault();
const message = input.value.trim();
if (!message) return;
appendMsg('user', message);
input.value = '';
input.style.height = 'auto';
setLoading(true);
const csrfToken = document.querySelector('input[name="_token"]')?.value
|| document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const body = { message };
if (threadId) body.thread_id = threadId;
try {
const res = await fetch('{{ route("agent.chat") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
'Accept': 'application/json',
},
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) {
appendMsg('error', data.message || 'Ocurrió un error. Intentá de nuevo.');
} else {
if (data.thread_id) threadId = data.thread_id;
appendMsg('assistant', data.reply);
}
} catch (err) {
appendMsg('error', 'No se pudo conectar con el agente. Verificá tu conexión.');
} finally {
setLoading(false);
input.focus();
}
});
rehydrate();
})();
</script>