CinePlex/js/chat.js

342 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 messageEl = document.createElement('div');
messageEl.classList.add('message', `${sender}-message`);
if(isError) messageEl.style.color = 'var(--danger)';
const p = document.createElement('p');
p.textContent = text;
messageEl.appendChild(p);
if (sender === 'assistant' && !isError) {
this.addMediaActionButtons(messageEl, text);
}
this.dom.messagesContainer.appendChild(messageEl);
this.scrollToBottom();
}
addTypingIndicator() {
const indicator = document.createElement('div');
indicator.id = 'typing-indicator';
indicator.classList.add('message', 'assistant-message', 'typing-indicator');
indicator.innerHTML = '<span></span><span></span><span></span>';
this.dom.messagesContainer.appendChild(indicator);
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');
}
}
}