2025-07-29 15:05:06 +02:00
|
|
|
import { state } from './state.js';
|
|
|
|
import { showNotification, _ } from './utils.js';
|
2025-07-30 11:19:29 +02:00
|
|
|
import { AITools } from './ai-tools.js';
|
2025-07-29 15:05:06 +02:00
|
|
|
|
|
|
|
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 = [];
|
2025-07-30 11:19:29 +02:00
|
|
|
this.aiTools = new AITools(this);
|
2025-07-29 15:05:06 +02:00
|
|
|
|
|
|
|
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) {
|
2025-07-30 11:19:29 +02:00
|
|
|
const welcomeMessage = _('chatWelcome');
|
|
|
|
this.addMessage(welcomeMessage, 'assistant');
|
|
|
|
this.conversationHistory.push({ role: 'assistant', content: welcomeMessage });
|
2025-07-29 15:05:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2025-07-30 11:19:29 +02:00
|
|
|
const response = await this.getOpenAIResponseWithTools();
|
2025-07-29 15:05:06 +02:00
|
|
|
this.removeTypingIndicator();
|
2025-07-30 11:19:29 +02:00
|
|
|
|
|
|
|
if (response) {
|
|
|
|
this.addMessage(response, 'assistant');
|
|
|
|
this.conversationHistory.push({ role: 'assistant', content: response });
|
|
|
|
}
|
|
|
|
|
2025-07-29 15:05:06 +02:00
|
|
|
} catch (error) {
|
|
|
|
this.removeTypingIndicator();
|
|
|
|
this.addMessage(error.message, 'assistant', true);
|
|
|
|
} finally {
|
|
|
|
this.dom.sendBtn.disabled = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-30 11:19:29 +02:00
|
|
|
addMessage(text, sender, isError = false, toolName = null) {
|
2025-07-29 17:15:23 +02:00
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
wrapper.className = `message-wrapper ${sender}-wrapper`;
|
|
|
|
|
2025-07-29 15:05:06 +02:00
|
|
|
const messageEl = document.createElement('div');
|
|
|
|
messageEl.classList.add('message', `${sender}-message`);
|
|
|
|
if(isError) messageEl.style.color = 'var(--danger)';
|
|
|
|
|
2025-07-30 11:19:29 +02:00
|
|
|
if (sender === 'assistant' || sender === 'tool-call' || sender === 'tool-result') {
|
2025-07-29 17:15:23 +02:00
|
|
|
const avatar = document.createElement('div');
|
|
|
|
avatar.className = 'avatar';
|
2025-07-30 11:19:29 +02:00
|
|
|
let icon = '';
|
|
|
|
if (sender === 'assistant') {
|
|
|
|
icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm-2.5-5h5v2h-5v-2z"/></svg>';
|
|
|
|
} else {
|
|
|
|
icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></svg>';
|
|
|
|
}
|
|
|
|
avatar.innerHTML = icon;
|
2025-07-29 17:15:23 +02:00
|
|
|
wrapper.appendChild(avatar);
|
|
|
|
}
|
|
|
|
|
2025-07-29 15:05:06 +02:00
|
|
|
const p = document.createElement('p');
|
2025-07-30 11:19:29 +02:00
|
|
|
if (sender === 'tool-call' || sender === 'tool-result') {
|
|
|
|
p.innerHTML = `<strong>${toolName}:</strong> ${text}`;
|
|
|
|
} else {
|
|
|
|
p.textContent = text;
|
2025-07-29 15:05:06 +02:00
|
|
|
}
|
2025-07-30 11:19:29 +02:00
|
|
|
messageEl.appendChild(p);
|
2025-07-29 15:05:06 +02:00
|
|
|
|
2025-07-29 17:15:23 +02:00
|
|
|
wrapper.appendChild(messageEl);
|
|
|
|
this.dom.messagesContainer.appendChild(wrapper);
|
2025-07-29 15:05:06 +02:00
|
|
|
this.scrollToBottom();
|
|
|
|
}
|
|
|
|
|
|
|
|
addTypingIndicator() {
|
2025-07-29 17:15:23 +02:00
|
|
|
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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm-2.5-5h5v2h-5v-2z"/></svg>`;
|
|
|
|
wrapper.appendChild(avatar);
|
|
|
|
|
2025-07-29 15:05:06 +02:00
|
|
|
const indicator = document.createElement('div');
|
2025-07-29 17:15:23 +02:00
|
|
|
indicator.classList.add('message', 'assistant-message', 'typing-indicator-bubble');
|
2025-07-29 15:05:06 +02:00
|
|
|
indicator.innerHTML = '<span></span><span></span><span></span>';
|
2025-07-29 17:15:23 +02:00
|
|
|
|
|
|
|
wrapper.appendChild(indicator);
|
|
|
|
this.dom.messagesContainer.appendChild(wrapper);
|
2025-07-29 15:05:06 +02:00
|
|
|
this.scrollToBottom();
|
|
|
|
}
|
|
|
|
|
|
|
|
removeTypingIndicator() {
|
|
|
|
const indicator = document.getElementById('typing-indicator');
|
|
|
|
if (indicator) indicator.remove();
|
|
|
|
}
|
|
|
|
|
2025-07-30 11:19:29 +02:00
|
|
|
async getOpenAIResponseWithTools() {
|
2025-07-29 15:05:06 +02:00
|
|
|
const apiKey = state.settings.openaiApiKey;
|
|
|
|
if (!apiKey) {
|
|
|
|
return _('chatApiKeyMissing');
|
|
|
|
}
|
2025-07-30 11:19:29 +02:00
|
|
|
|
|
|
|
const systemPrompt = _('aiSystemPrompt_v2');
|
2025-07-29 15:05:06 +02:00
|
|
|
const messagesForApi = [
|
2025-07-30 11:19:29 +02:00
|
|
|
{ role: 'system', content: systemPrompt },
|
2025-07-29 15:05:06 +02:00
|
|
|
...this.conversationHistory
|
|
|
|
];
|
2025-07-30 11:19:29 +02:00
|
|
|
|
2025-07-29 15:05:06 +02:00
|
|
|
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,
|
2025-07-30 11:19:29 +02:00
|
|
|
tools: this.aiTools.toolDefinitions,
|
|
|
|
tool_choice: 'auto',
|
|
|
|
max_tokens: 400,
|
2025-07-29 15:05:06 +02:00
|
|
|
temperature: 0.7
|
|
|
|
})
|
|
|
|
});
|
2025-07-30 11:19:29 +02:00
|
|
|
|
2025-07-29 15:05:06 +02:00
|
|
|
if (!response.ok) {
|
|
|
|
const errorData = await response.json().catch(() => ({}));
|
2025-07-30 11:19:29 +02:00
|
|
|
throw new Error(errorData?.error?.message || `Error HTTP ${response.status}`);
|
2025-07-29 15:05:06 +02:00
|
|
|
}
|
2025-07-30 11:19:29 +02:00
|
|
|
|
2025-07-29 15:05:06 +02:00
|
|
|
const data = await response.json();
|
2025-07-30 11:19:29 +02:00
|
|
|
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;
|
2025-07-29 15:05:06 +02:00
|
|
|
} else {
|
2025-07-30 11:19:29 +02:00
|
|
|
return _('chatApiInvalidResponse');
|
2025-07-29 15:05:06 +02:00
|
|
|
}
|
2025-07-30 11:19:29 +02:00
|
|
|
|
2025-07-29 15:05:06 +02:00
|
|
|
} catch (error) {
|
2025-07-30 11:19:29 +02:00
|
|
|
console.error('OpenAI API call failed:', error);
|
|
|
|
showNotification(_('chatApiError') + `: ${error.message}`, 'error');
|
2025-07-29 15:05:06 +02:00
|
|
|
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';
|
|
|
|
}
|
|
|
|
|
2025-07-30 11:19:29 +02:00
|
|
|
clearHistory() {
|
|
|
|
this.conversationHistory = [];
|
|
|
|
this.dom.messagesContainer.innerHTML = '';
|
|
|
|
const welcomeMessage = _('chatWelcome');
|
|
|
|
this.addMessage(welcomeMessage, 'assistant');
|
|
|
|
this.conversationHistory.push({ role: 'assistant', content: welcomeMessage });
|
2025-07-29 15:05:06 +02:00
|
|
|
}
|
2025-07-30 11:19:29 +02:00
|
|
|
}
|