/** * 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 = ` `; 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 = `
KiraBot
Asistente de TV · IA
`; 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 = '
Cargando...
'; 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 = '
No tienes grabaciones todavía.
¡Pídele a KiraBot que grabe algo!
'; 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 = `
${this._esc(r.title)}
${this._esc(r.channel_name || '')}${start ? ' · ' + start : ''}
${icon} ${label}
`; container.appendChild(item); }); } catch (err) { container.innerHTML = '
Error al cargar las grabaciones.
'; } } _esc(str) { return String(str || '').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); });