1139 lines
49 KiB
JavaScript
1139 lines
49 KiB
JavaScript
class AIDrawer {
|
||
constructor() {
|
||
this.isOpen = false;
|
||
this.fontSize = 'normal'; // По умолчанию нормальный размер шрифта
|
||
this.avatarType = 'default'; // Тип аватарки ассистента
|
||
this.preloadedHistory = null; // Кэш предзагруженной истории
|
||
this.historyLoaded = false; // Флаг загрузки истории
|
||
this.init();
|
||
|
||
// Предзагружаем историю сразу после инициализации
|
||
this.preloadChatHistory();
|
||
}
|
||
|
||
init() {
|
||
console.log('AI Drawer: Инициализация начата');
|
||
|
||
// Проверяем, не создан ли уже AI Drawer
|
||
if (document.querySelector('.ai-drawer')) {
|
||
console.log('AI Drawer: Уже существует, используем существующий');
|
||
this.drawer = document.querySelector('.ai-drawer');
|
||
this.toggleBtn = document.querySelector('.ai-drawer-toggle');
|
||
this.closeBtn = document.querySelector('.ai-drawer-close');
|
||
this.chatInput = document.getElementById('ai-chat-input');
|
||
this.sendButton = document.getElementById('ai-send-button');
|
||
this.messagesContainer = document.querySelector('.ai-messages-container');
|
||
this.loadingOverlay = document.querySelector('.ai-loading-overlay');
|
||
this.fontButtons = document.querySelectorAll('.font-btn');
|
||
this.avatarButtons = document.querySelectorAll('.avatar-btn');
|
||
|
||
// Настраиваем только обработчики событий для существующего drawer
|
||
this.setupEventListenersOnly();
|
||
this.restoreSettings();
|
||
this.initMobileHandlers();
|
||
this.setupResponsiveLayout();
|
||
console.log('AI Drawer: Переиспользование существующего завершено');
|
||
return;
|
||
}
|
||
|
||
// Простые стили
|
||
const style = document.createElement('style');
|
||
style.textContent =
|
||
'.ai-drawer-toggle {' +
|
||
'position: fixed !important;' +
|
||
'right: 18px !important;' +
|
||
'bottom: 20px !important;' +
|
||
'width: 50px !important;' +
|
||
'height: 50px !important;' +
|
||
'border-radius: 25px !important;' +
|
||
'background: #7c3aed !important;' +
|
||
'color: white !important;' +
|
||
'border: none !important;' +
|
||
'cursor: pointer !important;' +
|
||
'z-index: 999999 !important;' +
|
||
'display: flex !important;' +
|
||
'align-items: center !important;' +
|
||
'justify-content: center !important;' +
|
||
'font-weight: bold !important;' +
|
||
'font-size: 16px !important;' +
|
||
'}' +
|
||
'.ai-drawer {' +
|
||
'position: fixed !important;' +
|
||
'top: 0 !important;' +
|
||
'right: 0 !important;' +
|
||
'height: 100vh !important;' +
|
||
'width: 400px !important;' +
|
||
'background: #1a1a1a !important;' +
|
||
'border-left: 1px solid #333 !important;' +
|
||
'transform: translateX(100%) !important;' +
|
||
'transition: transform 0.3s ease !important;' +
|
||
'z-index: 999998 !important;' +
|
||
'display: flex !important;' +
|
||
'flex-direction: column !important;' +
|
||
'}' +
|
||
'.ai-drawer.open {' +
|
||
'transform: translateX(0) !important;' +
|
||
'}' +
|
||
'body.ai-drawer-open {' +
|
||
'margin-right: 400px !important;' +
|
||
'transition: margin-right 0.3s ease !important;' +
|
||
'}' +
|
||
'.ai-drawer-header {' +
|
||
'padding: 15px !important;' +
|
||
'background: #007bff !important;' +
|
||
'color: white !important;' +
|
||
'border-bottom: 1px solid #0056b3 !important;' +
|
||
'display: flex !important;' +
|
||
'justify-content: space-between !important;' +
|
||
'align-items: center !important;' +
|
||
'font-weight: 600 !important;' +
|
||
'}' +
|
||
'.ai-drawer-close {' +
|
||
'background: none !important;' +
|
||
'border: none !important;' +
|
||
'color: white !important;' +
|
||
'cursor: pointer !important;' +
|
||
'font-size: 20px !important;' +
|
||
'}' +
|
||
'.ai-drawer-content {' +
|
||
'flex: 1 !important;' +
|
||
'padding: 15px !important;' +
|
||
'color: white !important;' +
|
||
'}';
|
||
|
||
document.head.appendChild(style);
|
||
console.log('AI Drawer: Стили добавлены в head');
|
||
|
||
// Улучшенный HTML с панелью управления шрифтом
|
||
const drawerHTML =
|
||
'<button type="button" class="ai-drawer-toggle">AI</button>' +
|
||
'<div class="ai-drawer font-normal">' +
|
||
'<div class="ai-drawer-header">' +
|
||
'<span>AI Ассистент</span>' +
|
||
'<button type="button" class="ai-drawer-close">×</button>' +
|
||
'</div>' +
|
||
'<div class="ai-font-controls">' +
|
||
'<label>Размер шрифта:</label>' +
|
||
'<button type="button" class="font-btn" data-size="small">Мелкий</button>' +
|
||
'<button type="button" class="font-btn active" data-size="normal">Обычный</button>' +
|
||
'<button type="button" class="font-btn" data-size="large">Крупный</button>' +
|
||
'<button type="button" class="font-btn" data-size="extra-large">Очень крупный</button>' +
|
||
'</div>' +
|
||
'<div class="ai-avatar-controls">' +
|
||
'<label>Аватарка ассистента:</label>' +
|
||
'<button type="button" class="avatar-btn active" data-type="default">🤖</button>' +
|
||
'<button type="button" class="avatar-btn" data-type="friendly">😊</button>' +
|
||
'<button type="button" class="avatar-btn" data-type="helpful">💡</button>' +
|
||
'<button type="button" class="avatar-btn" data-type="smart">🧠</button>' +
|
||
'</div>' +
|
||
'<div class="ai-drawer-content">' +
|
||
'<div class="ai-messages-container">' +
|
||
'<div class="ai-message assistant">' +
|
||
'<div class="ai-avatar assistant"></div>' +
|
||
'<div class="ai-message-content">' +
|
||
'<p>Привет! Я ваш AI ассистент. Чем могу помочь?</p>' +
|
||
'<div class="ai-message-time">' + new Date().toLocaleTimeString() + '</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="ai-chat-input-container">' +
|
||
'<input type="text" id="ai-chat-input" placeholder="Введите сообщение..." class="ai-chat-input">' +
|
||
'<button type="button" id="ai-send-button" class="ai-send-button">Отправить</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="ai-loading-overlay">' +
|
||
'<div class="ai-loading-spinner"></div>' +
|
||
'<div>Обрабатываю запрос...</div>' +
|
||
'</div>';
|
||
|
||
// Добавляем в DOM
|
||
document.body.insertAdjacentHTML('beforeend', drawerHTML);
|
||
console.log('AI Drawer: HTML добавлен в DOM');
|
||
|
||
// Находим элементы
|
||
this.drawer = document.querySelector('.ai-drawer');
|
||
this.toggleBtn = document.querySelector('.ai-drawer-toggle');
|
||
this.closeBtn = document.querySelector('.ai-drawer-close');
|
||
this.loadingOverlay = document.querySelector('.ai-loading-overlay');
|
||
this.fontButtons = document.querySelectorAll('.font-btn');
|
||
this.avatarButtons = document.querySelectorAll('.avatar-btn');
|
||
this.chatInput = document.querySelector('#ai-chat-input');
|
||
this.sendButton = document.querySelector('#ai-send-button');
|
||
|
||
console.log('AI Drawer: Элементы найдены:', {
|
||
drawer: !!this.drawer,
|
||
toggleBtn: !!this.toggleBtn,
|
||
closeBtn: !!this.closeBtn,
|
||
loadingOverlay: !!this.loadingOverlay,
|
||
fontButtons: this.fontButtons.length
|
||
});
|
||
|
||
// Простые обработчики
|
||
if (this.toggleBtn) {
|
||
this.toggleBtn.onclick = (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('AI Drawer: Toggle button clicked');
|
||
this.toggle();
|
||
};
|
||
console.log('AI Drawer: Toggle обработчик добавлен');
|
||
}
|
||
|
||
if (this.closeBtn) {
|
||
this.closeBtn.onclick = (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('AI Drawer: Close button clicked');
|
||
this.close();
|
||
};
|
||
console.log('AI Drawer: Close обработчик добавлен');
|
||
}
|
||
|
||
// Обработчики для кнопок управления шрифтом
|
||
this.fontButtons.forEach(button => {
|
||
button.onclick = () => {
|
||
const size = button.dataset.size;
|
||
this.setFontSize(size);
|
||
|
||
// Обновляем активную кнопку
|
||
this.fontButtons.forEach(btn => btn.classList.remove('active'));
|
||
button.classList.add('active');
|
||
};
|
||
});
|
||
console.log('AI Drawer: Font buttons обработчики добавлены');
|
||
|
||
// Обработчики для кнопок управления аватаркой
|
||
this.avatarButtons.forEach(button => {
|
||
button.onclick = () => {
|
||
const type = button.dataset.type;
|
||
this.setAvatarType(type);
|
||
|
||
// Обновляем активную кнопку
|
||
this.avatarButtons.forEach(btn => btn.classList.remove('active'));
|
||
button.classList.add('active');
|
||
};
|
||
});
|
||
console.log('AI Drawer: Avatar buttons обработчики добавлены');
|
||
|
||
// Обработчики для поля ввода
|
||
if (this.sendButton && this.chatInput) {
|
||
this.sendButton.onclick = (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.sendMessage();
|
||
};
|
||
|
||
this.chatInput.onkeypress = (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
this.sendMessage();
|
||
}
|
||
};
|
||
}
|
||
console.log('AI Drawer: Chat input обработчики добавлены');
|
||
|
||
// Восстанавливаем сохраненные настройки
|
||
this.restoreSettings();
|
||
|
||
// Добавляем обработчики для мобильных устройств
|
||
this.initMobileHandlers();
|
||
this.setupResponsiveLayout();
|
||
}
|
||
|
||
// Метод для настройки только обработчиков событий (без создания HTML)
|
||
setupEventListenersOnly() {
|
||
console.log('AI Drawer: Настройка обработчиков для существующего drawer');
|
||
|
||
// Простые обработчики
|
||
if (this.toggleBtn) {
|
||
this.toggleBtn.onclick = (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('AI Drawer: Toggle button clicked');
|
||
this.toggle();
|
||
};
|
||
console.log('AI Drawer: Toggle обработчик добавлен');
|
||
}
|
||
|
||
if (this.closeBtn) {
|
||
this.closeBtn.onclick = (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('AI Drawer: Close button clicked');
|
||
this.close();
|
||
};
|
||
console.log('AI Drawer: Close обработчик добавлен');
|
||
}
|
||
|
||
// Обработчики для кнопок управления шрифтом
|
||
this.fontButtons.forEach(button => {
|
||
button.onclick = () => {
|
||
const size = button.dataset.size;
|
||
this.setFontSize(size);
|
||
|
||
// Обновляем активную кнопку
|
||
this.fontButtons.forEach(btn => btn.classList.remove('active'));
|
||
button.classList.add('active');
|
||
};
|
||
});
|
||
console.log('AI Drawer: Font buttons обработчики добавлены');
|
||
|
||
// Обработчики для кнопок управления аватаркой
|
||
this.avatarButtons.forEach(button => {
|
||
button.onclick = () => {
|
||
const type = button.dataset.type;
|
||
this.setAvatarType(type);
|
||
|
||
// Обновляем активную кнопку
|
||
this.avatarButtons.forEach(btn => btn.classList.remove('active'));
|
||
button.classList.add('active');
|
||
};
|
||
});
|
||
console.log('AI Drawer: Avatar buttons обработчики добавлены');
|
||
|
||
// Обработчики для поля ввода
|
||
if (this.sendButton && this.chatInput) {
|
||
this.sendButton.onclick = (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.sendMessage();
|
||
};
|
||
|
||
this.chatInput.onkeypress = (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
this.sendMessage();
|
||
}
|
||
};
|
||
}
|
||
console.log('AI Drawer: Chat input обработчики добавлены');
|
||
}
|
||
|
||
toggle() {
|
||
console.log('AI Drawer: Toggle called, isOpen:', this.isOpen);
|
||
if (this.isOpen) {
|
||
this.close();
|
||
} else {
|
||
this.open();
|
||
}
|
||
}
|
||
|
||
open() {
|
||
console.log('AI Drawer: Opening drawer');
|
||
try {
|
||
if (this.drawer) {
|
||
this.drawer.classList.add('open');
|
||
console.log('AI Drawer: Added open class');
|
||
} else {
|
||
console.error('AI Drawer: Drawer element not found!');
|
||
return;
|
||
}
|
||
|
||
document.body.classList.add('ai-drawer-open');
|
||
this.isOpen = true;
|
||
console.log('AI Drawer: Set isOpen to true');
|
||
|
||
// Автоматически загружаем историю при открытии
|
||
setTimeout(() => {
|
||
console.log('AI Drawer: Starting chat initialization');
|
||
this.initializeChat().catch(error => {
|
||
console.error('AI Drawer: Initialization failed:', error);
|
||
});
|
||
}, 500);
|
||
|
||
console.log('AI Drawer: Drawer opened successfully');
|
||
} catch (error) {
|
||
console.error('AI Drawer: Error in open():', error);
|
||
}
|
||
}
|
||
|
||
close() {
|
||
console.log('AI Drawer: Closing drawer');
|
||
if (this.drawer) {
|
||
this.drawer.classList.remove('open');
|
||
}
|
||
|
||
document.body.classList.remove('ai-drawer-open');
|
||
this.isOpen = false;
|
||
|
||
console.log('AI Drawer: Drawer closed');
|
||
}
|
||
|
||
// Метод для изменения размера шрифта
|
||
setFontSize(size) {
|
||
console.log('AI Drawer: Setting font size to:', size);
|
||
|
||
if (this.drawer) {
|
||
// Убираем все классы размеров шрифта
|
||
this.drawer.classList.remove('font-small', 'font-normal', 'font-large', 'font-extra-large');
|
||
// Добавляем новый класс
|
||
this.drawer.classList.add('font-' + size);
|
||
}
|
||
|
||
this.fontSize = size;
|
||
|
||
// Сохраняем настройку в localStorage
|
||
localStorage.setItem('ai-drawer-font-size', size);
|
||
|
||
console.log('AI Drawer: Font size changed to:', size);
|
||
}
|
||
|
||
// Метод для показа плавающего индикатора загрузки
|
||
showLoading(message = 'Обрабатываю запрос...') {
|
||
console.log('AI Drawer: Showing loading overlay');
|
||
if (this.loadingOverlay) {
|
||
const textElement = this.loadingOverlay.querySelector('div:last-child');
|
||
if (textElement) {
|
||
textElement.textContent = message;
|
||
}
|
||
this.loadingOverlay.classList.add('show');
|
||
}
|
||
}
|
||
|
||
// Метод для скрытия плавающего индикатора загрузки
|
||
hideLoading() {
|
||
console.log('AI Drawer: Hiding loading overlay');
|
||
if (this.loadingOverlay) {
|
||
this.loadingOverlay.classList.remove('show');
|
||
}
|
||
}
|
||
|
||
// Метод для отображения истории (из предзагрузки или по запросу)
|
||
displayHistory(data) {
|
||
try {
|
||
console.log('AI Drawer: Displaying history, success:', data.success, 'messages:', data.history ? data.history.length : 0);
|
||
|
||
// Скрываем индикатор загрузки
|
||
this.hideLoading();
|
||
|
||
if (data.success && data.history && data.history.length > 0) {
|
||
// Очищаем текущие сообщения
|
||
this.clearMessages();
|
||
|
||
// Загружаем историю
|
||
data.history.forEach(msg => {
|
||
try {
|
||
console.log('AI Drawer: Adding history message:', msg.type, msg.message.substring(0, 30) + '...');
|
||
this.addMessage(msg.message, msg.type === 'user', msg.timestamp);
|
||
} catch (error) {
|
||
console.error('AI Drawer: Error adding history message:', error, msg);
|
||
}
|
||
});
|
||
|
||
console.log(`AI Drawer: Displayed ${data.history.length} messages from history`);
|
||
} else {
|
||
// Если истории нет, показываем приветственное сообщение
|
||
this.clearMessages();
|
||
const projectName = data.context?.projectName || 'проектом';
|
||
this.addStreamingMessage(`Привет! Я ваш AI ассистент. Работаем с "${projectName}". Чем могу помочь?`, false, 25);
|
||
}
|
||
} catch (error) {
|
||
console.error('AI Drawer: Error displaying history:', error);
|
||
this.hideLoading();
|
||
this.addStreamingMessage('Привет! Я готов к работе. Чем могу помочь?', false, 25);
|
||
}
|
||
}
|
||
|
||
// Метод для очистки сообщений
|
||
clearMessages() {
|
||
const content = this.drawer.querySelector('.ai-messages-container');
|
||
if (content) {
|
||
content.innerHTML = '';
|
||
console.log('AI Drawer: Messages cleared');
|
||
}
|
||
}
|
||
|
||
// Метод для добавления сообщения в чат
|
||
addMessage(text, isUser = false, customTime = null) {
|
||
console.log('AI Drawer: Adding message:', text.substring(0, 50) + '...');
|
||
|
||
const content = this.drawer.querySelector('.ai-messages-container');
|
||
if (content) {
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = `ai-message ${isUser ? 'user' : 'assistant'}`;
|
||
|
||
// Создаем аватарку
|
||
const avatarDiv = document.createElement('div');
|
||
let avatarClass = `ai-avatar ${isUser ? 'user' : 'assistant'}`;
|
||
|
||
// Добавляем тип аватарки для ассистента
|
||
if (!isUser && this.avatarType !== 'default') {
|
||
avatarClass += ` ${this.avatarType}`;
|
||
}
|
||
|
||
avatarDiv.className = avatarClass;
|
||
|
||
// Для пользователя добавляем первую букву имени или инициал
|
||
if (isUser) {
|
||
avatarDiv.textContent = '👤'; // Или можно использовать первую букву имени
|
||
}
|
||
|
||
// Создаем контейнер для контента
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.className = 'ai-message-content';
|
||
|
||
// Создаем параграф с текстом
|
||
const textDiv = document.createElement('p');
|
||
textDiv.textContent = text;
|
||
contentDiv.appendChild(textDiv);
|
||
|
||
// Создаем время
|
||
const timeDiv = document.createElement('div');
|
||
timeDiv.className = 'ai-message-time';
|
||
timeDiv.textContent = customTime || new Date().toLocaleTimeString();
|
||
contentDiv.appendChild(timeDiv);
|
||
|
||
// Собираем сообщение
|
||
messageDiv.appendChild(avatarDiv);
|
||
messageDiv.appendChild(contentDiv);
|
||
|
||
content.appendChild(messageDiv);
|
||
|
||
// Автоматически прокручиваем к последнему сообщению
|
||
this.scrollToBottom();
|
||
}
|
||
}
|
||
|
||
// Метод для добавления сообщения со стримингом
|
||
addStreamingMessage(text, isUser = false, speed = 30) {
|
||
console.log('AI Drawer: Adding streaming message:', text.substring(0, 50) + '...');
|
||
|
||
const content = this.drawer.querySelector('.ai-messages-container');
|
||
if (content) {
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = `ai-message ${isUser ? 'user' : 'assistant'}`;
|
||
|
||
// Создаем аватарку
|
||
const avatarDiv = document.createElement('div');
|
||
let avatarClass = `ai-avatar ${isUser ? 'user' : 'assistant'}`;
|
||
|
||
// Добавляем тип аватарки для ассистента
|
||
if (!isUser && this.avatarType !== 'default') {
|
||
avatarClass += ` ${this.avatarType}`;
|
||
}
|
||
|
||
avatarDiv.className = avatarClass;
|
||
|
||
// Для пользователя добавляем первую букву имени или инициал
|
||
if (isUser) {
|
||
avatarDiv.textContent = '👤';
|
||
}
|
||
|
||
// Создаем контейнер для контента
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.className = 'ai-message-content';
|
||
|
||
// Создаем параграф с текстом (пока пустой)
|
||
const textDiv = document.createElement('p');
|
||
textDiv.textContent = '';
|
||
contentDiv.appendChild(textDiv);
|
||
|
||
// Создаем время
|
||
const timeDiv = document.createElement('div');
|
||
timeDiv.className = 'ai-message-time';
|
||
timeDiv.textContent = new Date().toLocaleTimeString();
|
||
contentDiv.appendChild(timeDiv);
|
||
|
||
// Собираем сообщение
|
||
messageDiv.appendChild(avatarDiv);
|
||
messageDiv.appendChild(contentDiv);
|
||
|
||
content.appendChild(messageDiv);
|
||
|
||
// Автоматически прокручиваем к последнему сообщению
|
||
this.scrollToBottom();
|
||
|
||
// Запускаем стриминг
|
||
this.streamText(textDiv, text, speed);
|
||
}
|
||
}
|
||
|
||
// Метод для стриминга текста
|
||
streamText(element, text, speed = 30) {
|
||
let index = 0;
|
||
const interval = setInterval(() => {
|
||
if (index < text.length) {
|
||
element.textContent += text[index];
|
||
index++;
|
||
|
||
// Прокручиваем к последнему сообщению
|
||
const content = this.drawer.querySelector('.ai-messages-container');
|
||
if (content) {
|
||
this.scrollToBottom();
|
||
}
|
||
} else {
|
||
clearInterval(interval);
|
||
}
|
||
}, speed);
|
||
}
|
||
|
||
// Метод для показа индикатора печатания
|
||
showTypingIndicator() {
|
||
const content = this.drawer.querySelector('.ai-messages-container');
|
||
if (content) {
|
||
// Удаляем предыдущий индикатор если есть
|
||
const existingIndicator = content.querySelector('.ai-typing-indicator');
|
||
if (existingIndicator) {
|
||
existingIndicator.remove();
|
||
}
|
||
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = 'ai-message assistant';
|
||
|
||
// Создаем аватарку
|
||
const avatarDiv = document.createElement('div');
|
||
let avatarClass = `ai-avatar assistant`;
|
||
if (this.avatarType !== 'default') {
|
||
avatarClass += ` ${this.avatarType}`;
|
||
}
|
||
avatarDiv.className = avatarClass;
|
||
|
||
// Создаем контейнер для контента
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.className = 'ai-message-content';
|
||
|
||
// Создаем индикатор печатания
|
||
const typingDiv = document.createElement('div');
|
||
typingDiv.className = 'ai-typing-indicator';
|
||
typingDiv.innerHTML = `
|
||
<div class="ai-typing-dots">
|
||
<div class="ai-typing-dot"></div>
|
||
<div class="ai-typing-dot"></div>
|
||
<div class="ai-typing-dot"></div>
|
||
</div>
|
||
<span class="ai-typing-text">печатает...</span>
|
||
`;
|
||
|
||
contentDiv.appendChild(typingDiv);
|
||
|
||
// Собираем сообщение
|
||
messageDiv.appendChild(avatarDiv);
|
||
messageDiv.appendChild(contentDiv);
|
||
|
||
content.appendChild(messageDiv);
|
||
|
||
// Автоматически прокручиваем к последнему сообщению
|
||
this.scrollToBottom();
|
||
|
||
return messageDiv;
|
||
}
|
||
}
|
||
|
||
// Метод для скрытия индикатора печатания
|
||
hideTypingIndicator() {
|
||
const content = this.drawer.querySelector('.ai-messages-container');
|
||
if (content) {
|
||
const typingIndicator = content.querySelector('.ai-typing-indicator');
|
||
if (typingIndicator) {
|
||
const messageDiv = typingIndicator.closest('.ai-message');
|
||
if (messageDiv) {
|
||
messageDiv.remove();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Метод для смены аватарки ассистента
|
||
setAvatarType(type) {
|
||
console.log('AI Drawer: Setting avatar type to:', type);
|
||
this.avatarType = type;
|
||
|
||
// Обновляем существующие аватарки ассистента
|
||
const existingAvatars = this.drawer.querySelectorAll('.ai-avatar.assistant');
|
||
existingAvatars.forEach(avatar => {
|
||
// Убираем старые классы типов
|
||
avatar.classList.remove('friendly', 'helpful', 'smart');
|
||
|
||
// Добавляем новый класс типа
|
||
if (type !== 'default') {
|
||
avatar.classList.add(type);
|
||
}
|
||
});
|
||
|
||
// Сохраняем настройку
|
||
localStorage.setItem('ai-drawer-avatar-type', type);
|
||
|
||
console.log('AI Drawer: Avatar type changed to:', type);
|
||
}
|
||
|
||
// Метод для отправки сообщения
|
||
sendMessage() {
|
||
if (!this.chatInput || !this.sendButton) return;
|
||
|
||
const message = this.chatInput.value.trim();
|
||
if (!message) return;
|
||
|
||
console.log('AI Drawer: Sending message:', message);
|
||
|
||
// Добавляем сообщение пользователя
|
||
this.addMessage(message, true);
|
||
|
||
// Очищаем поле ввода
|
||
this.chatInput.value = '';
|
||
|
||
// Показываем индикатор загрузки
|
||
this.showLoading('🤖 Обрабатываю ваш запрос...');
|
||
|
||
// Отправляем запрос в n8n
|
||
this.sendToN8N(message);
|
||
}
|
||
|
||
// Метод для отправки сообщения в n8n
|
||
async sendToN8N(message, isInitialization = false) {
|
||
try {
|
||
console.log('AI Drawer: Sending to n8n:', message);
|
||
|
||
// Получаем контекст CRM
|
||
const context = this.getCurrentContext();
|
||
|
||
// Показываем индикатор печатания
|
||
this.showTypingIndicator();
|
||
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 120000); // 2 минуты таймаут
|
||
|
||
const response = await fetch('/aiassist/n8n_proxy.php', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
message: message,
|
||
context: context,
|
||
sessionId: 'ai-drawer-session-' + Date.now()
|
||
}),
|
||
signal: controller.signal
|
||
});
|
||
|
||
clearTimeout(timeoutId);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`N8N Proxy error: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('AI Drawer: n8n response:', data);
|
||
|
||
// Дополнительное логирование для отладки
|
||
console.log('AI Drawer: Response details:', {
|
||
hasError: !!data.error,
|
||
hasResponse: !!data.response,
|
||
responseType: typeof data.response,
|
||
responseLength: data.response ? data.response.length : 0,
|
||
allKeys: Object.keys(data)
|
||
});
|
||
|
||
if (data.error) {
|
||
throw new Error(data.error);
|
||
}
|
||
|
||
// Скрываем индикатор загрузки и печатания
|
||
this.hideLoading();
|
||
this.hideTypingIndicator();
|
||
|
||
// Добавляем ответ ассистента со стримингом
|
||
if (data.response) {
|
||
this.addStreamingMessage(data.response, false, 25);
|
||
} else {
|
||
this.addStreamingMessage('Получен ответ от n8n, но сообщение пустое', false, 25);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('AI Drawer: n8n error:', error);
|
||
|
||
// Скрываем индикаторы
|
||
this.hideLoading();
|
||
this.hideTypingIndicator();
|
||
|
||
// Определяем тип ошибки
|
||
let errorMessage = 'Извините, произошла ошибка при обработке запроса. Попробуйте еще раз.';
|
||
|
||
if (error.name === 'AbortError') {
|
||
errorMessage = 'Превышено время ожидания ответа. Попробуйте еще раз.';
|
||
} else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
|
||
errorMessage = 'Проблема с подключением к серверу. Проверьте интернет-соединение.';
|
||
}
|
||
|
||
// Показываем сообщение об ошибке со стримингом
|
||
this.addStreamingMessage(errorMessage, false, 25);
|
||
}
|
||
}
|
||
|
||
// Метод для получения контекста CRM
|
||
getCurrentContext() {
|
||
console.log('AI Drawer: getCurrentContext() called');
|
||
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const projectId = urlParams.get('record') || '';
|
||
|
||
console.log('AI Drawer: URL params:', {
|
||
record: urlParams.get('record'),
|
||
module: urlParams.get('module'),
|
||
view: urlParams.get('view')
|
||
});
|
||
|
||
// Получаем данные из URL
|
||
const currentModule = urlParams.get('module') || '';
|
||
const currentView = urlParams.get('view') || '';
|
||
|
||
// Получаем данные из глобальных переменных CRM
|
||
let userId = '';
|
||
let userName = '';
|
||
let userEmail = '';
|
||
|
||
console.log('AI Drawer: Checking global variables:', {
|
||
'_USERMETA exists': typeof _USERMETA !== 'undefined',
|
||
'_USERMETA.id': typeof _USERMETA !== 'undefined' ? _USERMETA.id : 'N/A',
|
||
'_META exists': typeof _META !== 'undefined',
|
||
'_META.module': typeof _META !== 'undefined' ? _META.module : 'N/A'
|
||
});
|
||
|
||
if (typeof _USERMETA !== 'undefined') {
|
||
userId = _USERMETA.id || '';
|
||
userName = _USERMETA.name || '';
|
||
userEmail = _USERMETA.email || '';
|
||
}
|
||
|
||
// Получаем название проекта/компании
|
||
let projectName = '';
|
||
try {
|
||
const recordLabel = document.querySelector('.recordLabel, .record-name, h1');
|
||
if (recordLabel) {
|
||
projectName = recordLabel.textContent.trim();
|
||
}
|
||
} catch (e) {
|
||
console.log('AI Drawer: Could not get project name:', e);
|
||
}
|
||
|
||
// Получаем заголовок страницы
|
||
let pageTitle = document.title || '';
|
||
|
||
// Получаем текущую дату
|
||
const currentDate = new Date().toLocaleDateString('ru-RU');
|
||
|
||
const context = {
|
||
projectId: projectId,
|
||
currentModule: currentModule,
|
||
currentView: currentView,
|
||
userId: userId,
|
||
userName: userName,
|
||
userEmail: userEmail,
|
||
projectName: projectName,
|
||
pageTitle: pageTitle,
|
||
currentDate: currentDate,
|
||
url: window.location.href,
|
||
timestamp: Date.now()
|
||
};
|
||
|
||
console.log('AI Drawer: Context data:', context);
|
||
return context;
|
||
}
|
||
|
||
// Метод для инициализации чата при открытии
|
||
async initializeChat() {
|
||
try {
|
||
console.log('AI Drawer: Initializing chat with context');
|
||
|
||
// Показываем индикатор загрузки только если история не предзагружена
|
||
if (!this.historyLoaded) {
|
||
this.showLoading('📜 Загружаю историю диалога...');
|
||
}
|
||
|
||
// Загружаем историю (предзагруженную или по запросу)
|
||
await this.loadChatHistory();
|
||
|
||
} catch (error) {
|
||
console.error('AI Drawer: Chat initialization error:', error);
|
||
this.hideLoading();
|
||
this.addStreamingMessage('Привет! Я готов к работе. Чем могу помочь?', false, 25);
|
||
}
|
||
}
|
||
|
||
// Метод для предзагрузки истории чата (в фоне)
|
||
async preloadChatHistory() {
|
||
try {
|
||
console.log('AI Drawer: Preloading chat history in background');
|
||
|
||
// Получаем контекст CRM
|
||
const context = this.getCurrentContext();
|
||
const sessionId = 'ai-drawer-session-' + context.projectId + '-' + context.userId;
|
||
|
||
console.log('AI Drawer: Preloading for session:', sessionId);
|
||
|
||
const response = await fetch('/get_chat_history.php', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
context: context,
|
||
sessionId: sessionId
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
this.preloadedHistory = data;
|
||
this.historyLoaded = true;
|
||
console.log('AI Drawer: History preloaded successfully, messages count:', data.history ? data.history.length : 0);
|
||
} else {
|
||
console.warn('AI Drawer: History preload failed:', response.status);
|
||
this.preloadedHistory = null;
|
||
this.historyLoaded = false;
|
||
}
|
||
} catch (error) {
|
||
console.warn('AI Drawer: History preload error (will load on demand):', error);
|
||
this.preloadedHistory = null;
|
||
this.historyLoaded = false;
|
||
}
|
||
}
|
||
|
||
// Метод для обновления предзагруженной истории (при смене модуля/записи)
|
||
refreshPreloadedHistory() {
|
||
console.log('AI Drawer: Refreshing preloaded history');
|
||
this.preloadedHistory = null;
|
||
this.historyLoaded = false;
|
||
this.preloadChatHistory();
|
||
}
|
||
|
||
// Метод для загрузки истории чата
|
||
async loadChatHistory() {
|
||
try {
|
||
console.log('AI Drawer: Loading chat history');
|
||
|
||
// Проверяем что drawer открыт
|
||
if (!this.isOpen) {
|
||
console.log('AI Drawer: Drawer is not open, skipping history load');
|
||
return;
|
||
}
|
||
|
||
// Если история уже предзагружена, используем её
|
||
if (this.historyLoaded && this.preloadedHistory) {
|
||
console.log('AI Drawer: Using preloaded history');
|
||
this.displayHistory(this.preloadedHistory);
|
||
return;
|
||
}
|
||
|
||
console.log('AI Drawer: History not preloaded, loading on demand');
|
||
|
||
// Получаем контекст CRM
|
||
const context = this.getCurrentContext();
|
||
console.log('AI Drawer: Context for history:', context);
|
||
|
||
// Запрашиваем историю
|
||
const sessionId = 'ai-drawer-session-' + context.projectId + '-' + context.userId;
|
||
console.log('AI Drawer: Sending history request to /get_chat_history.php');
|
||
console.log('AI Drawer: Request payload:', { context, sessionId });
|
||
|
||
const response = await fetch('/get_chat_history.php', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
context: context,
|
||
sessionId: sessionId
|
||
})
|
||
});
|
||
|
||
console.log('AI Drawer: Response status:', response.status, response.statusText);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`History request failed: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('AI Drawer: History loaded:', data);
|
||
|
||
// Отображаем загруженную историю
|
||
this.displayHistory(data);
|
||
|
||
} catch (error) {
|
||
console.error('AI Drawer: History loading error:', error);
|
||
this.hideLoading();
|
||
|
||
// Показываем fallback сообщение
|
||
this.clearMessages();
|
||
this.addStreamingMessage('Привет! Я готов к работе. Чем могу помочь?', false, 25);
|
||
}
|
||
}
|
||
|
||
// Метод для восстановления настроек из localStorage
|
||
restoreSettings() {
|
||
const savedFontSize = localStorage.getItem('ai-drawer-font-size');
|
||
if (savedFontSize && savedFontSize !== this.fontSize) {
|
||
this.setFontSize(savedFontSize);
|
||
|
||
// Обновляем активную кнопку
|
||
this.fontButtons.forEach(btn => {
|
||
btn.classList.remove('active');
|
||
if (btn.dataset.size === savedFontSize) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Восстанавливаем тип аватарки
|
||
const savedAvatarType = localStorage.getItem('ai-drawer-avatar-type');
|
||
if (savedAvatarType && savedAvatarType !== this.avatarType) {
|
||
this.setAvatarType(savedAvatarType);
|
||
|
||
// Обновляем активную кнопку аватарки
|
||
this.avatarButtons.forEach(btn => {
|
||
btn.classList.remove('active');
|
||
if (btn.dataset.type === savedAvatarType) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Метод для инициализации обработчиков мобильных устройств
|
||
initMobileHandlers() {
|
||
console.log('AI Drawer: Initializing mobile handlers');
|
||
|
||
// Проверяем, что мы на мобильном устройстве
|
||
const isMobile = window.innerWidth <= 768;
|
||
if (!isMobile) return;
|
||
|
||
// Обработчик изменения размера viewport (для виртуальной клавиатуры)
|
||
let initialViewportHeight = window.innerHeight;
|
||
let isKeyboardVisible = false;
|
||
|
||
const handleResize = () => {
|
||
// Проверяем что все еще мобильное устройство
|
||
if (window.innerWidth <= 768) {
|
||
const currentHeight = window.innerHeight;
|
||
const heightDifference = initialViewportHeight - currentHeight;
|
||
|
||
// Если высота уменьшилась более чем на 100px, считаем что клавиатура появилась
|
||
if (heightDifference > 100 && !isKeyboardVisible) {
|
||
isKeyboardVisible = true;
|
||
this.handleKeyboardShow();
|
||
console.log('AI Drawer: Keyboard detected, height difference:', heightDifference);
|
||
}
|
||
// Если высота вернулась к исходной, считаем что клавиатура скрылась
|
||
else if (heightDifference < 30 && isKeyboardVisible) {
|
||
isKeyboardVisible = false;
|
||
this.handleKeyboardHide();
|
||
console.log('AI Drawer: Keyboard hidden, height difference:', heightDifference);
|
||
}
|
||
}
|
||
};
|
||
|
||
window.addEventListener('resize', handleResize);
|
||
|
||
// Обработчик фокуса на поле ввода
|
||
if (this.chatInput) {
|
||
this.chatInput.addEventListener('focus', () => {
|
||
console.log('AI Drawer: Input focused on mobile');
|
||
setTimeout(() => {
|
||
this.handleKeyboardShow();
|
||
this.scrollToBottom();
|
||
}, 300); // Даем время клавиатуре появиться
|
||
});
|
||
|
||
this.chatInput.addEventListener('blur', () => {
|
||
console.log('AI Drawer: Input blurred on mobile');
|
||
setTimeout(() => {
|
||
this.handleKeyboardHide();
|
||
}, 300); // Даем время клавиатуре скрыться
|
||
});
|
||
}
|
||
|
||
// Обработчик для предотвращения зума на iOS
|
||
if (this.chatInput) {
|
||
this.chatInput.addEventListener('focus', () => {
|
||
// Устанавливаем размер шрифта 16px для предотвращения зума
|
||
this.chatInput.style.fontSize = '16px';
|
||
});
|
||
}
|
||
|
||
console.log('AI Drawer: Mobile handlers initialized');
|
||
}
|
||
|
||
// Обработчик появления виртуальной клавиатуры
|
||
handleKeyboardShow() {
|
||
console.log('AI Drawer: Virtual keyboard shown');
|
||
|
||
// Проверяем что это мобильное устройство
|
||
if (window.innerWidth <= 768 && this.drawer && this.drawer.classList.contains('open')) {
|
||
// Добавляем класс для адаптации к клавиатуре
|
||
this.drawer.classList.add('keyboard-visible');
|
||
|
||
// Динамически определяем высоту клавиатуры и поднимаем весь drawer
|
||
const screenHeight = window.screen.height;
|
||
const viewportHeight = window.innerHeight;
|
||
const keyboardHeight = screenHeight - viewportHeight;
|
||
|
||
// Поднимаем весь drawer на высоту клавиатуры + запас
|
||
const liftAmount = Math.max(300, keyboardHeight + 150);
|
||
this.drawer.style.transform = `translateY(-${liftAmount}px)`;
|
||
|
||
console.log('AI Drawer: Keyboard detected, lifting drawer', {
|
||
screenHeight,
|
||
viewportHeight,
|
||
keyboardHeight,
|
||
liftAmount
|
||
});
|
||
|
||
// Прокручиваем к последнему сообщению
|
||
setTimeout(() => {
|
||
this.scrollToBottom();
|
||
}, 200);
|
||
}
|
||
}
|
||
|
||
// Обработчик скрытия виртуальной клавиатуры
|
||
handleKeyboardHide() {
|
||
console.log('AI Drawer: Virtual keyboard hidden');
|
||
|
||
// Проверяем что это мобильное устройство
|
||
if (window.innerWidth <= 768 && this.drawer) {
|
||
// Убираем класс адаптации к клавиатуре
|
||
this.drawer.classList.remove('keyboard-visible');
|
||
|
||
// Возвращаем drawer в исходное положение
|
||
this.drawer.style.transform = 'translateY(0px)';
|
||
}
|
||
}
|
||
|
||
// Настройка адаптивной структуры
|
||
setupResponsiveLayout() {
|
||
const isMobile = window.innerWidth <= 768;
|
||
const inputContainer = this.drawer.querySelector('.ai-chat-input-container');
|
||
const content = this.drawer.querySelector('.ai-drawer-content');
|
||
|
||
if (isMobile) {
|
||
// На мобильных - перемещаем поле ввода внутрь content
|
||
if (inputContainer && content && !content.querySelector('.ai-chat-input-container')) {
|
||
content.appendChild(inputContainer);
|
||
console.log('AI Drawer: Moved input container inside content for mobile');
|
||
}
|
||
} else {
|
||
// На десктопе - перемещаем поле ввода обратно в drawer
|
||
if (inputContainer && content && content.querySelector('.ai-chat-input-container')) {
|
||
this.drawer.appendChild(inputContainer);
|
||
console.log('AI Drawer: Moved input container back to drawer for desktop');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Метод для прокрутки к последнему сообщению
|
||
scrollToBottom() {
|
||
const content = this.drawer.querySelector('.ai-messages-container');
|
||
if (content) {
|
||
// Используем requestAnimationFrame для более плавной прокрутки
|
||
requestAnimationFrame(() => {
|
||
content.scrollTop = content.scrollHeight;
|
||
|
||
// Дополнительная проверка через небольшую задержку
|
||
setTimeout(() => {
|
||
content.scrollTop = content.scrollHeight;
|
||
}, 100);
|
||
});
|
||
}
|
||
}
|
||
}
|