import { state } from './state.js'; import { showNotification, _ } from './utils.js'; import { getFromDB } from './db.js'; import { showItemDetails, addStreamToList, downloadM3U } from './ui.js'; export class Chat { constructor() { this.dom = { fab: document.getElementById('chat-fab'), window: document.getElementById('chat-window'), header: document.querySelector('.chat-header'), closeBtn: document.getElementById('chat-close-btn'), messagesContainer: document.getElementById('chat-messages'), inputForm: document.getElementById('chat-input-form'), input: document.getElementById('chat-input'), sendBtn: document.getElementById('chat-send-btn') }; this.isOpen = false; this.isDragging = false; this.offset = { x: 0, y: 0 }; this.conversationHistory = []; this.bindEvents(); } bindEvents() { this.dom.fab.addEventListener('click', () => this.toggle()); this.dom.closeBtn.addEventListener('click', () => this.close()); this.dom.inputForm.addEventListener('submit', (e) => { e.preventDefault(); this.sendMessage(); }); this.dom.input.addEventListener('input', this.autoResizeTextarea.bind(this)); this.dom.input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); this.dom.header.addEventListener('mousedown', this.startDrag.bind(this)); document.addEventListener('mousemove', this.drag.bind(this)); document.addEventListener('mouseup', this.stopDrag.bind(this)); } toggle() { this.isOpen ? this.close() : this.open(); } open() { if (this.isOpen) return; this.isOpen = true; this.dom.window.style.top = ''; this.dom.window.style.left = ''; this.dom.window.style.bottom = '95px'; this.dom.window.style.right = '2rem'; this.dom.window.style.display = 'flex'; gsap.fromTo(this.dom.window, { opacity: 0, scale: 0.9, y: 20 }, { opacity: 1, scale: 1, y: 0, duration: 0.3, ease: 'power3.out' }); gsap.to(this.dom.fab, { scale: 0, opacity: 0, duration: 0.2, ease: 'power2.in' }); if (this.conversationHistory.length === 0) { this.addMessage(_('chatWelcome'), 'assistant'); this.conversationHistory.push({ role: 'assistant', content: _('chatWelcome') }); } } close() { if (!this.isOpen) return; this.isOpen = false; gsap.to(this.dom.window, { opacity: 0, scale: 0.9, y: 20, duration: 0.3, ease: 'power2.in', onComplete: () => { this.dom.window.style.display = 'none'; }}); gsap.fromTo(this.dom.fab, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)', delay: 0.2 }); } async sendMessage() { const userInput = this.dom.input.value.trim(); if (!userInput) return; this.addMessage(userInput, 'user'); this.conversationHistory.push({ role: 'user', content: userInput }); this.dom.input.value = ''; this.autoResizeTextarea(); this.dom.sendBtn.disabled = true; this.addTypingIndicator(); try { const response = await this.getOpenAIResponse(); this.removeTypingIndicator(); this.addMessage(response, 'assistant'); this.conversationHistory.push({ role: 'assistant', content: response }); } catch (error) { this.removeTypingIndicator(); this.addMessage(error.message, 'assistant', true); } finally { this.dom.sendBtn.disabled = false; } } addMessage(text, sender, isError = false) { const wrapper = document.createElement('div'); wrapper.className = `message-wrapper ${sender}-wrapper`; const messageEl = document.createElement('div'); messageEl.classList.add('message', `${sender}-message`); if(isError) messageEl.style.color = 'var(--danger)'; if (sender === 'assistant') { const avatar = document.createElement('div'); avatar.className = 'avatar'; avatar.innerHTML = ``; wrapper.appendChild(avatar); } const p = document.createElement('p'); p.textContent = text; messageEl.appendChild(p); if (sender === 'assistant' && !isError) { this.addMediaActionButtons(messageEl, text); } wrapper.appendChild(messageEl); this.dom.messagesContainer.appendChild(wrapper); this.scrollToBottom(); } addTypingIndicator() { const wrapper = document.createElement('div'); wrapper.id = 'typing-indicator'; wrapper.className = 'message-wrapper assistant-wrapper'; const avatar = document.createElement('div'); avatar.className = 'avatar'; avatar.innerHTML = ``; wrapper.appendChild(avatar); const indicator = document.createElement('div'); indicator.classList.add('message', 'assistant-message', 'typing-indicator-bubble'); indicator.innerHTML = ''; wrapper.appendChild(indicator); this.dom.messagesContainer.appendChild(wrapper); this.scrollToBottom(); } removeTypingIndicator() { const indicator = document.getElementById('typing-indicator'); if (indicator) indicator.remove(); } async getLocalContent() { const movieEntries = await getFromDB('movies'); const seriesEntries = await getFromDB('series'); const processEntries = (entries, type) => { return entries.flatMap(entry => (entry.titulos || []).map(titulo => ({ ...titulo, type })) ); }; const movies = processEntries(movieEntries, 'movie'); const series = processEntries(seriesEntries, 'show'); return { movies, series }; } async findLocalContent(title) { const { movies, series } = await this.getLocalContent(); const searchTerm = title.trim().toLowerCase(); const allContent = [...movies, ...series]; let foundContent = allContent.find(m => m.title.toLowerCase() === searchTerm); if (foundContent) return foundContent; foundContent = allContent.find(m => m.title.toLowerCase().includes(searchTerm)); return foundContent; } async getOpenAIResponse() { const apiKey = state.settings.openaiApiKey; if (!apiKey) { return _('chatApiKeyMissing'); } const systemPrompt = `You are a helpful assistant for a movie and TV show enthusiast named CinePlex Assistant. Your main goal is to provide information about films, series, etc. When you identify a movie or series title in your response, you MUST enclose it in double quotes, for example: "The Matrix". Do not mention the user's local library or your ability to access it.`; const messagesForApi = [ { role: 'system', content: systemPrompt }, ...this.conversationHistory ]; try { const response = await fetch('https://api.proxyapi.ru/openai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: 'gpt-4o-2024-05-13', messages: messagesForApi, max_tokens: 250, temperature: 0.7 }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMessage = errorData?.error?.message || errorData?.error || `Error HTTP ${response.status}`; console.error('Proxy API Error Response:', errorData); throw new Error(errorMessage); } const data = await response.json(); if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) { return data.choices[0].message.content; } else { console.error('Invalid successful response format from proxy:', data); throw new Error(_('chatApiInvalidResponse')); } } catch (error) { console.error('OpenAI Proxy API call failed:', error); showNotification(_('chatApiError'), 'error'); return _('chatApiError') + `: ${error.message}`; } } autoResizeTextarea() { this.dom.input.style.height = 'auto'; const scrollHeight = this.dom.input.scrollHeight; if (scrollHeight > 200) { this.dom.input.style.height = '200px'; this.dom.input.style.overflowY = 'auto'; } else { this.dom.input.style.height = `${scrollHeight}px`; this.dom.input.style.overflowY = 'hidden'; } } scrollToBottom() { this.dom.messagesContainer.scrollTop = this.dom.messagesContainer.scrollHeight; } startDrag(e) { if (e.target !== this.dom.header && e.target !== this.dom.header.querySelector('.chat-title')) return; this.isDragging = true; const rect = this.dom.window.getBoundingClientRect(); this.offset.x = e.clientX - rect.left; this.offset.y = e.clientY - rect.top; this.dom.header.style.cursor = 'grabbing'; } drag(e) { if (!this.isDragging) return; e.preventDefault(); let newX = e.clientX - this.offset.x; let newY = e.clientY - this.offset.y; const winWidth = this.dom.window.offsetWidth; const winHeight = this.dom.window.offsetHeight; const docWidth = document.documentElement.clientWidth; const docHeight = document.documentElement.clientHeight; newX = Math.max(0, Math.min(newX, docWidth - winWidth)); newY = Math.max(0, Math.min(newY, docHeight - winHeight)); this.dom.window.style.left = `${newX}px`; this.dom.window.style.top = `${newY}px`; this.dom.window.style.bottom = 'auto'; this.dom.window.style.right = 'auto'; } stopDrag() { this.isDragging = false; this.dom.header.style.cursor = 'move'; } async addMediaActionButtons(messageEl, text) { const contentTitles = this.extractContentTitles(text); if (contentTitles.length === 0) return; const allContent = []; for (const title of contentTitles) { const content = await this.findLocalContent(title); if (content) { allContent.push(content); } } if (allContent.length > 0) { allContent.forEach(content => { const itemContainer = document.createElement('div'); itemContainer.className = 'chat-item-actions'; const titleEl = document.createElement('span'); titleEl.className = 'chat-action-title'; titleEl.textContent = content.title; itemContainer.appendChild(titleEl); const buttonContainer = document.createElement('div'); buttonContainer.className = 'chat-action-buttons'; const buttons = [ { label: _('moreInfo'), action: 'info', icon: 'ℹ️' }, { label: _('addStream'), action: 'stream', icon: '▶️' }, { label: _('download'), action: 'download', icon: '⬇️' } ]; buttons.forEach(btnInfo => { const button = document.createElement('button'); button.innerHTML = `${btnInfo.icon} ${btnInfo.label}`; button.addEventListener('click', () => this.handleMediaAction(btnInfo.action, content, button)); buttonContainer.appendChild(button); }); itemContainer.appendChild(buttonContainer); messageEl.appendChild(itemContainer); }); if (allContent.length > 1) { const downloadAllButtonContainer = document.createElement('div'); downloadAllButtonContainer.className = 'chat-action-buttons chat-download-all'; const downloadAllButton = document.createElement('button'); downloadAllButton.innerHTML = `⬇️ ${_('downloadAll')}`; downloadAllButton.addEventListener('click', (e) => { const streams = allContent.map(content => ({ title: content.title, type: content.type })); downloadM3U(streams, e.target); }); downloadAllButtonContainer.appendChild(downloadAllButton); messageEl.appendChild(downloadAllButtonContainer); } } } extractContentTitles(text) { const matches = text.match(/"([^"]+)"/g); if (!matches) return []; return matches.map(match => match.substring(1, match.length - 1)); } handleMediaAction(action, content, buttonEl) { switch (action) { case 'info': showItemDetails(Number(content.id), content.type); this.close(); break; case 'stream': addStreamToList(content.title, content.type, buttonEl); break; case 'download': downloadM3U(content.title, content.type, buttonEl); break; default: showNotification(`Acción desconocida: ${action}`, 'warning'); } } }