Files
KiraTV-APP-NodeCast-Modific…/public/js/components/ChatBot.js
T
2026-07-13 15:18:05 +00:00

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
}
// 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);
});