import { state } from './state.js'; import { showNotification, _ } from './utils.js'; import { AITools } from './ai-tools.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.aiTools = new AITools(this); 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) { const welcomeMessage = _('chatWelcome'); this.addMessage(welcomeMessage, 'assistant'); this.conversationHistory.push({ role: 'assistant', content: welcomeMessage }); } } 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.getOpenAIResponseWithTools(); this.removeTypingIndicator(); if (response) { 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, toolName = null) { 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' || sender === 'tool-call' || sender === 'tool-result') { const avatar = document.createElement('div'); avatar.className = 'avatar'; let icon = ''; if (sender === 'assistant') { icon = ''; } else { icon = ''; } avatar.innerHTML = icon; wrapper.appendChild(avatar); } const p = document.createElement('p'); if (sender === 'tool-call' || sender === 'tool-result') { p.innerHTML = `${toolName}: ${text}`; } else { p.textContent = text; } messageEl.appendChild(p); 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 getOpenAIResponseWithTools() { const apiKey = state.settings.openaiApiKey; if (!apiKey) { return _('chatApiKeyMissing'); } const systemPrompt = _('aiSystemPrompt_v3'); 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-3.5-turbo', messages: messagesForApi, tools: this.aiTools.toolDefinitions, tool_choice: 'auto', max_tokens: 400, temperature: 0.7 }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData?.error?.message || `Error HTTP ${response.status}`); } const data = await response.json(); const message = data.choices[0].message; if (message.tool_calls) { this.conversationHistory.push(message); for (const toolCall of message.tool_calls) { const toolResult = await this.aiTools.executeTool(toolCall); this.conversationHistory.push({ role: 'tool', tool_call_id: toolCall.id, name: toolCall.function.name, content: toolResult }); } // Now call the API again with the tool results return await this.getOpenAIResponseWithTools(); } else if (message.content) { return message.content; } else { return _('chatApiInvalidResponse'); } } catch (error) { console.error('OpenAI API call failed:', error); showNotification(_('chatApiError') + `: ${error.message}`, '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'; } clearHistory() { this.conversationHistory = []; this.dom.messagesContainer.innerHTML = ''; const welcomeMessage = _('chatWelcome'); this.addMessage(welcomeMessage, 'assistant'); this.conversationHistory.push({ role: 'assistant', content: welcomeMessage }); } }