429 lines
16 KiB
JavaScript
429 lines
16 KiB
JavaScript
/**
|
|
* KiraBot — floating chat widget for KiraTV
|
|
*/
|
|
class ChatBot {
|
|
constructor() {
|
|
this._open = false;
|
|
this._tab = 'chat'; // 'chat' | 'recordings'
|
|
this._messages = [];
|
|
this._recordings = [];
|
|
this._loading = false;
|
|
this._token = null;
|
|
this._init();
|
|
}
|
|
|
|
_init() {
|
|
// Inject styles
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
#kirabot-btn {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
z-index: 9000;
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
|
border: none;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 20px rgba(124,58,237,.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: transform .2s, box-shadow .2s;
|
|
}
|
|
#kirabot-btn:hover { transform: scale(1.1); box-shadow: 0 6px 28px rgba(124,58,237,.7); }
|
|
#kirabot-btn svg { width: 26px; height: 26px; fill: #fff; }
|
|
#kirabot-badge {
|
|
position: absolute;
|
|
top: -4px;
|
|
right: -4px;
|
|
background: #ef4444;
|
|
color: #fff;
|
|
border-radius: 9999px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
min-width: 18px;
|
|
height: 18px;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0 4px;
|
|
}
|
|
#kirabot-panel {
|
|
position: fixed;
|
|
bottom: 90px;
|
|
right: 24px;
|
|
z-index: 9001;
|
|
width: 380px;
|
|
max-width: calc(100vw - 32px);
|
|
height: 560px;
|
|
max-height: calc(100vh - 110px);
|
|
background: #1a1a2e;
|
|
border: 1px solid rgba(124,58,237,.4);
|
|
border-radius: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 16px 48px rgba(0,0,0,.6);
|
|
transition: opacity .25s, transform .25s;
|
|
overflow: hidden;
|
|
}
|
|
#kirabot-panel.hidden { opacity: 0; transform: translateY(12px) scale(.97); pointer-events: none; }
|
|
.kb-header {
|
|
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.kb-header-title { font-weight: 700; font-size: 15px; color: #fff; flex: 1; }
|
|
.kb-header-sub { font-size: 11px; color: rgba(255,255,255,.7); }
|
|
.kb-close-btn {
|
|
background: rgba(255,255,255,.15);
|
|
border: none;
|
|
border-radius: 50%;
|
|
width: 28px; height: 28px;
|
|
cursor: pointer;
|
|
color: #fff;
|
|
font-size: 16px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.kb-tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid rgba(255,255,255,.08);
|
|
background: #16162a;
|
|
}
|
|
.kb-tab {
|
|
flex: 1;
|
|
padding: 10px;
|
|
background: none;
|
|
border: none;
|
|
color: rgba(255,255,255,.5);
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: color .2s;
|
|
}
|
|
.kb-tab.active { color: #a78bfa; border-bottom: 2px solid #7c3aed; font-weight: 600; }
|
|
.kb-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
scroll-behavior: smooth;
|
|
}
|
|
.kb-messages::-webkit-scrollbar { width: 4px; }
|
|
.kb-messages::-webkit-scrollbar-track { background: transparent; }
|
|
.kb-messages::-webkit-scrollbar-thumb { background: rgba(255,255,255,.15); border-radius: 2px; }
|
|
.kb-bubble {
|
|
max-width: 88%;
|
|
padding: 10px 13px;
|
|
border-radius: 14px;
|
|
font-size: 13.5px;
|
|
line-height: 1.5;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
.kb-bubble.user {
|
|
align-self: flex-end;
|
|
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
|
color: #fff;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
.kb-bubble.bot {
|
|
align-self: flex-start;
|
|
background: rgba(255,255,255,.07);
|
|
color: #e2e8f0;
|
|
border-bottom-left-radius: 4px;
|
|
}
|
|
.kb-bubble.error { background: rgba(239,68,68,.15); color: #fca5a5; border: 1px solid rgba(239,68,68,.3); }
|
|
.kb-typing { align-self: flex-start; color: rgba(255,255,255,.4); font-size: 13px; font-style: italic; }
|
|
.kb-input-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 12px 14px;
|
|
border-top: 1px solid rgba(255,255,255,.08);
|
|
background: #16162a;
|
|
}
|
|
.kb-input {
|
|
flex: 1;
|
|
background: rgba(255,255,255,.07);
|
|
border: 1px solid rgba(255,255,255,.12);
|
|
border-radius: 12px;
|
|
color: #e2e8f0;
|
|
font-size: 13.5px;
|
|
padding: 8px 12px;
|
|
resize: none;
|
|
outline: none;
|
|
font-family: inherit;
|
|
}
|
|
.kb-input:focus { border-color: rgba(124,58,237,.6); }
|
|
.kb-send-btn {
|
|
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
|
border: none;
|
|
border-radius: 12px;
|
|
width: 42px;
|
|
height: 42px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.kb-send-btn:disabled { opacity: .4; cursor: default; }
|
|
.kb-send-btn svg { width: 18px; height: 18px; fill: #fff; }
|
|
.kb-recordings {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.kb-rec-item {
|
|
background: rgba(255,255,255,.05);
|
|
border: 1px solid rgba(255,255,255,.08);
|
|
border-radius: 12px;
|
|
padding: 10px 12px;
|
|
}
|
|
.kb-rec-title { font-weight: 600; font-size: 13px; color: #e2e8f0; }
|
|
.kb-rec-meta { font-size: 11px; color: rgba(255,255,255,.45); margin-top: 3px; }
|
|
.kb-rec-status { font-size: 11px; font-weight: 600; margin-top: 4px; }
|
|
.kb-rec-status.scheduled { color: #fbbf24; }
|
|
.kb-rec-status.recording { color: #f87171; }
|
|
.kb-rec-status.completed { color: #34d399; }
|
|
.kb-rec-status.failed { color: #f87171; }
|
|
.kb-rec-status.cancelled { color: rgba(255,255,255,.3); }
|
|
.kb-empty { text-align: center; color: rgba(255,255,255,.35); font-size: 13px; padding: 40px 0; }
|
|
.kb-bot-avatar { width: 22px; height: 22px; flex-shrink: 0; }
|
|
#kb-chat-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; min-height: 0; }
|
|
#kb-rec-view { display: none; flex-direction: column; flex: 1; overflow: hidden; min-height: 0; }
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// Floating button
|
|
const btn = document.createElement('button');
|
|
btn.id = 'kirabot-btn';
|
|
btn.title = 'KiraBot — Asistente de TV';
|
|
btn.innerHTML = `
|
|
<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM9 11H7V9h2v2zm4 0h-2V9h2v2zm4 0h-2V9h2v2z"/></svg>
|
|
<span id="kirabot-badge"></span>
|
|
`;
|
|
btn.addEventListener('click', () => this.toggle());
|
|
document.body.appendChild(btn);
|
|
|
|
// Chat panel
|
|
const panel = document.createElement('div');
|
|
panel.id = 'kirabot-panel';
|
|
panel.className = 'hidden';
|
|
panel.innerHTML = `
|
|
<div class="kb-header">
|
|
<svg class="kb-bot-avatar" viewBox="0 0 24 24" fill="white"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
|
|
<div>
|
|
<div class="kb-header-title">KiraBot</div>
|
|
<div class="kb-header-sub">Asistente de TV · IA</div>
|
|
</div>
|
|
<button class="kb-close-btn" id="kb-close">✕</button>
|
|
</div>
|
|
<div class="kb-tabs">
|
|
<button class="kb-tab active" id="kb-tab-chat">💬 Chat</button>
|
|
<button class="kb-tab" id="kb-tab-rec">📹 Grabaciones</button>
|
|
</div>
|
|
<div id="kb-chat-view">
|
|
<div class="kb-messages" id="kb-messages"></div>
|
|
<div class="kb-input-row">
|
|
<textarea class="kb-input" id="kb-input" rows="1"
|
|
placeholder="Pregunta por la programación, pide grabaciones..."></textarea>
|
|
<button class="kb-send-btn" id="kb-send" title="Enviar">
|
|
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="kb-rec-view" style="display:none;flex:1;overflow:hidden;flex-direction:column;">
|
|
<div class="kb-recordings" id="kb-recordings"></div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(panel);
|
|
|
|
// Events
|
|
document.getElementById('kb-close').addEventListener('click', () => this.close());
|
|
document.getElementById('kb-tab-chat').addEventListener('click', () => this._switchTab('chat'));
|
|
document.getElementById('kb-tab-rec').addEventListener('click', () => this._switchTab('recordings'));
|
|
document.getElementById('kb-send').addEventListener('click', () => this._sendMessage());
|
|
|
|
// Stop all keyboard and mouse events from bubbling to NodeCast's global handlers
|
|
// (NodeCast intercepts spacebar for play/pause and other keys for media control)
|
|
const inputEl = document.getElementById('kb-input');
|
|
inputEl.addEventListener('keydown', (e) => {
|
|
e.stopPropagation();
|
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this._sendMessage(); }
|
|
});
|
|
inputEl.addEventListener('keypress', (e) => e.stopPropagation());
|
|
inputEl.addEventListener('keyup', (e) => e.stopPropagation());
|
|
|
|
panel.addEventListener('click', (e) => e.stopPropagation());
|
|
panel.addEventListener('mousedown', (e) => e.stopPropagation());
|
|
|
|
// Welcome message
|
|
this._appendBotMessage('¡Hola! Soy KiraBot 👋 Puedo ayudarte con la programación de TV y programar grabaciones. ¿En qué te puedo ayudar?');
|
|
}
|
|
|
|
toggle() {
|
|
this._open ? this.close() : this.open();
|
|
}
|
|
|
|
open() {
|
|
this._open = true;
|
|
document.getElementById('kirabot-panel').classList.remove('hidden');
|
|
document.getElementById('kb-input').focus();
|
|
this._scrollToBottom();
|
|
}
|
|
|
|
close() {
|
|
this._open = false;
|
|
document.getElementById('kirabot-panel').classList.add('hidden');
|
|
}
|
|
|
|
_switchTab(tab) {
|
|
this._tab = tab;
|
|
document.getElementById('kb-tab-chat').classList.toggle('active', tab === 'chat');
|
|
document.getElementById('kb-tab-rec').classList.toggle('active', tab === 'recordings');
|
|
document.getElementById('kb-chat-view').style.display = tab === 'chat' ? 'flex' : 'none';
|
|
document.getElementById('kb-rec-view').style.display = tab === 'recordings' ? 'flex' : 'none';
|
|
if (tab === 'recordings') this._loadRecordings();
|
|
}
|
|
|
|
_appendBotMessage(text, isError = false) {
|
|
const msgs = document.getElementById('kb-messages');
|
|
const div = document.createElement('div');
|
|
div.className = `kb-bubble bot${isError ? ' error' : ''}`;
|
|
div.textContent = text;
|
|
msgs.appendChild(div);
|
|
this._scrollToBottom();
|
|
return div;
|
|
}
|
|
|
|
_appendUserMessage(text) {
|
|
const msgs = document.getElementById('kb-messages');
|
|
const div = document.createElement('div');
|
|
div.className = 'kb-bubble user';
|
|
div.textContent = text;
|
|
msgs.appendChild(div);
|
|
this._scrollToBottom();
|
|
}
|
|
|
|
_showTyping() {
|
|
const msgs = document.getElementById('kb-messages');
|
|
const div = document.createElement('div');
|
|
div.className = 'kb-typing';
|
|
div.id = 'kb-typing';
|
|
div.textContent = 'KiraBot está escribiendo...';
|
|
msgs.appendChild(div);
|
|
this._scrollToBottom();
|
|
}
|
|
|
|
_removeTyping() {
|
|
document.getElementById('kb-typing')?.remove();
|
|
}
|
|
|
|
_scrollToBottom() {
|
|
const msgs = document.getElementById('kb-messages');
|
|
if (msgs) setTimeout(() => { msgs.scrollTop = msgs.scrollHeight; }, 10);
|
|
}
|
|
|
|
async _sendMessage() {
|
|
const input = document.getElementById('kb-input');
|
|
const text = input.value.trim();
|
|
if (!text || this._loading) return;
|
|
|
|
input.value = '';
|
|
input.style.height = 'auto';
|
|
this._appendUserMessage(text);
|
|
this._loading = true;
|
|
document.getElementById('kb-send').disabled = true;
|
|
this._showTyping();
|
|
|
|
try {
|
|
const resp = await fetch('/api/kirabot/chat', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('authToken') || ''}`,
|
|
},
|
|
body: JSON.stringify({ message: text }),
|
|
});
|
|
const data = await resp.json();
|
|
this._removeTyping();
|
|
if (resp.ok) {
|
|
this._appendBotMessage(data.response || 'Sin respuesta');
|
|
} else {
|
|
this._appendBotMessage(data.error || 'Error al conectar con KiraBot', true);
|
|
}
|
|
} catch (err) {
|
|
this._removeTyping();
|
|
this._appendBotMessage('No se pudo conectar con KiraBot. ¿Está el servidor activo?', true);
|
|
} finally {
|
|
this._loading = false;
|
|
document.getElementById('kb-send').disabled = false;
|
|
input.focus();
|
|
}
|
|
}
|
|
|
|
async _loadRecordings() {
|
|
const container = document.getElementById('kb-recordings');
|
|
container.innerHTML = '<div class="kb-empty">Cargando...</div>';
|
|
|
|
try {
|
|
const resp = await fetch('/api/kirabot/recordings?limit=20', {
|
|
headers: { 'Authorization': `Bearer ${localStorage.getItem('authToken') || ''}` },
|
|
});
|
|
const recs = await resp.json();
|
|
container.innerHTML = '';
|
|
|
|
if (!Array.isArray(recs) || recs.length === 0) {
|
|
container.innerHTML = '<div class="kb-empty">No tienes grabaciones todavía.<br>¡Pídele a KiraBot que grabe algo!</div>';
|
|
return;
|
|
}
|
|
|
|
const statusLabels = {
|
|
scheduled: ['⏳', 'scheduled', 'Programada'],
|
|
recording: ['🔴', 'recording', 'Grabando'],
|
|
completed: ['✓', 'completed', 'Completada'],
|
|
failed: ['✗', 'failed', 'Fallida'],
|
|
cancelled: ['✗', 'cancelled', 'Cancelada'],
|
|
};
|
|
|
|
recs.forEach((r) => {
|
|
const [icon, cls, label] = statusLabels[r.status] || ['?', '', r.status];
|
|
const start = r.start_time ? new Date(r.start_time).toLocaleString('es-ES', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
|
|
const item = document.createElement('div');
|
|
item.className = 'kb-rec-item';
|
|
item.innerHTML = `
|
|
<div class="kb-rec-title">${this._esc(r.title)}</div>
|
|
<div class="kb-rec-meta">${this._esc(r.channel_name || '')}${start ? ' · ' + start : ''}</div>
|
|
<div class="kb-rec-status ${cls}">${icon} ${label}</div>
|
|
`;
|
|
container.appendChild(item);
|
|
});
|
|
} catch (err) {
|
|
container.innerHTML = '<div class="kb-empty" style="color:#f87171">Error al cargar las grabaciones.</div>';
|
|
}
|
|
}
|
|
|
|
_esc(str) {
|
|
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
}
|
|
|
|
// Initialize when app is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Delay slightly to let auth check happen first
|
|
setTimeout(() => {
|
|
if (localStorage.getItem('authToken')) {
|
|
window.kiraBot = new ChatBot();
|
|
}
|
|
}, 500);
|
|
});
|