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.querySelector('.ai-chat-input');
this.sendButton = document.querySelector('.ai-send-btn');
this.messagesContainer = document.querySelector('.ai-chat-messages');
this.loadingOverlay = document.querySelector('.ai-loading-overlay');
this.fontButtons = document.querySelectorAll('.font-btn');
this.avatarButtons = document.querySelectorAll('.avatar-btn');
// Перемещаем ассистента в конец body, чтобы исключить перекрытия родителями
try {
if (this.drawer && document.body && this.drawer.parentElement !== document.body) {
document.body.appendChild(this.drawer);
}
if (this.toggleBtn && document.body && this.toggleBtn.parentElement !== document.body) {
document.body.appendChild(this.toggleBtn);
}
} catch (e) {
console.warn('AI Drawer: Failed to move nodes to body', e);
}
// Настраиваем только обработчики событий для существующего 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: 2147483647 !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: #ffffff !important;' +
'border-left: 1px solid #333 !important;' +
'transform: translateX(100%) !important;' +
'transition: transform 0.3s ease !important;' +
'z-index: 2147483647 !important;' +
'display: flex !important;' +
'flex-direction: column !important;' +
'isolation: isolate !important;' +
'contain: layout style paint !important;' +
'will-change: transform !important;' +
'}' +
'.ai-drawer.open {' +
'transform: translateX(0) !important;' +
'z-index: 2147483647 !important;' +
'}' +
'.ai-drawer * {' +
'z-index: 2147483647 !important;' +
'position: relative !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;' +
'z-index: 2147483647 !important;' +
'position: relative !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;' +
'display: flex !important;' +
'flex-direction: column !important;' +
'color: white !important;' +
'}' +
'.ai-drawer-body {' +
'flex: 1 !important;' +
'display: flex !important;' +
'flex-direction: column !important;' +
'overflow: hidden !important;' +
'}' +
'.ai-chat-container {' +
'flex: 1 !important;' +
'display: flex !important;' +
'flex-direction: column !important;' +
'height: 100% !important;' +
'}' +
'.ai-chat-messages {' +
'flex: 1 !important;' +
'overflow-y: auto !important;' +
'padding: 15px !important;' +
'background: #ffffff !important;' +
'border-radius: 0 !important;' +
'margin-bottom: 0 !important;' +
'}' +
'.ai-chat-input-container {' +
'display: flex !important;' +
'gap: 10px !important;' +
'align-items: flex-end !important;' +
'padding: 15px !important;' +
'background: #f8f9fa !important;' +
'border-top: 1px solid #dee2e6 !important;' +
'}' +
'.ai-chat-input {' +
'flex: 1 !important;' +
'min-height: 40px !important;' +
'max-height: 120px !important;' +
'padding: 10px !important;' +
'border: 1px solid #555 !important;' +
'border-radius: 5px !important;' +
'background: #3a3a3a !important;' +
'color: white !important;' +
'resize: vertical !important;' +
'font-family: inherit !important;' +
'font-size: 14px !important;' +
'}' +
'.ai-chat-input:focus {' +
'outline: none !important;' +
'border-color: #007bff !important;' +
'}' +
'.ai-send-btn {' +
'padding: 10px 20px !important;' +
'background: #007bff !important;' +
'color: white !important;' +
'border: none !important;' +
'border-radius: 5px !important;' +
'cursor: pointer !important;' +
'font-weight: 600 !important;' +
'transition: background 0.2s ease !important;' +
'}' +
'.ai-send-btn:hover {' +
'background: #0056b3 !important;' +
'}' +
'.ai-send-btn:disabled {' +
'background: #6c757d !important;' +
'cursor: not-allowed !important;' +
'}' +
'.ai-message {' +
'margin-bottom: 15px !important;' +
'display: flex !important;' +
'align-items: flex-start !important;' +
'gap: 10px !important;' +
'}' +
'.ai-avatar {' +
'width: 30px !important;' +
'height: 30px !important;' +
'border-radius: 50% !important;' +
'display: flex !important;' +
'align-items: center !important;' +
'justify-content: center !important;' +
'font-size: 14px !important;' +
'flex-shrink: 0 !important;' +
'}' +
'.ai-avatar.user {' +
'background: #007bff !important;' +
'color: white !important;' +
'}' +
'.ai-avatar.assistant {' +
'background: #28a745 !important;' +
'color: white !important;' +
'}' +
'.ai-message-content {' +
'flex: 1 !important;' +
'background: #3a3a3a !important;' +
'padding: 10px 15px !important;' +
'border-radius: 10px !important;' +
'max-width: 80% !important;' +
'}' +
'.ai-message.user .ai-message-content {' +
'background: #007bff !important;' +
'color: white !important;' +
'}' +
'.ai-message-time {' +
'font-size: 11px !important;' +
'color: #999 !important;' +
'margin-top: 5px !important;' +
'text-align: right !important;' +
'}' +
'.ai-controls-panel, .ai-font-controls, .ai-avatar-controls {' +
'background: #f8f9fa !important;' +
'padding: 10px 15px !important;' +
'border-bottom: 1px solid #dee2e6 !important;' +
'display: flex !important;' +
'gap: 20px !important;' +
'align-items: center !important;' +
'height: 80px !important;' +
'z-index: 2147483647 !important;' +
'position: relative !important;' +
'margin-bottom: 10px !important;' +
'}' +
'.ai-controls-group, .ai-font-controls, .ai-avatar-controls {' +
'display: flex !important;' +
'align-items: center !important;' +
'gap: 8px !important;' +
'}' +
'.ai-controls-group span, .ai-font-controls label, .ai-avatar-controls label {' +
'color: #ccc !important;' +
'font-size: 12px !important;' +
'font-weight: 600 !important;' +
'}' +
'.font-btn, .avatar-btn {' +
'width: 40px !important;' +
'height: 40px !important;' +
'border: 1px solid #ced4da !important;' +
'border-radius: 8px !important;' +
'background: #ffffff !important;' +
'color: #000 !important;' +
'cursor: pointer !important;' +
'display: flex !important;' +
'align-items: center !important;' +
'justify-content: center !important;' +
'font-size: 16px !important;' +
'transition: all 0.2s ease !important;' +
'font-weight: bold !important;' +
'margin: 2px !important;' +
'}' +
'.font-btn:hover, .avatar-btn:hover {' +
'background: #ffffff !important;' +
'border-color: #777 !important;' +
'}' +
'.font-btn.active, .avatar-btn.active {' +
'background: #007bff !important;' +
'border-color: #007bff !important;' +
'}' +
'.font-btn {' +
'background: #ffffff !important;' +
'color: #000 !important;' +
'border: 1px solid #ced4da !important;' +
'}' +
'.font-btn:hover {' +
'background: #f8f9fa !important;' +
'}' +
'.font-btn.active {' +
'background: #007bff !important;' +
'color: #fff !important;' +
'}' +
'.font-btn[data-size="small"] { font-size: 10px !important; }' +
'.font-btn[data-size="normal"] { font-size: 12px !important; }' +
'.font-btn[data-size="large"] { font-size: 14px !important; }' +
'.font-btn[data-size="xlarge"] { font-size: 16px !important; }' +
// Мобильные стили
'@media (max-width: 768px) {' +
'.ai-drawer {' +
'width: 100vw !important;' +
'height: 100vh !important;' +
'top: 0 !important;' +
'right: 0 !important;' +
'transform: translateX(100%) !important;' +
'}' +
'.ai-drawer.open {' +
'transform: translateX(0) !important;' +
'}' +
'.ai-drawer.keyboard-visible {' +
'transform: translateX(0) translateY(-300px) !important;' +
'}' +
'body.ai-drawer-open {' +
'margin-right: 0 !important;' +
'}' +
'.ai-controls-panel, .ai-font-controls, .ai-avatar-controls {' +
'flex-direction: column !important;' +
'gap: 10px !important;' +
'height: auto !important;' +
'padding: 15px !important;' +
'}' +
'.ai-controls-group, .ai-font-controls, .ai-avatar-controls {' +
'flex-wrap: wrap !important;' +
'justify-content: center !important;' +
'}' +
'.font-btn, .avatar-btn {' +
'width: 35px !important;' +
'height: 35px !important;' +
'font-size: 16px !important;' +
'}' +
'.ai-chat-input {' +
'font-size: 16px !important;' +
'min-height: 50px !important;' +
'}' +
'.ai-send-btn {' +
'padding: 15px 25px !important;' +
'font-size: 16px !important;' +
'}' +
'}' +
// Принудительное отображение поверх всех элементов
'.ai-drawer, .ai-drawer-toggle {' +
'z-index: 2147483647 !important;' +
'position: fixed !important;' +
'}' +
'.ai-drawer * {' +
'z-index: 2147483646 !important;' +
'position: relative !important;' +
'}' +
'.ai-drawer {' +
'position: fixed !important;' +
'}' +
'.ai-drawer-toggle {' +
'position: fixed !important;' +
'}' +
// Переопределение z-index для всех возможных элементов CRM
'.modal, .popup, .overlay, .dropdown, .tooltip, .notification {' +
'z-index: 999999 !important;' +
'}';
document.head.appendChild(style);
console.log('AI Drawer: Стили добавлены в head');
// Улучшенный HTML с панелью управления шрифтом
const drawerHTML =
'' +
'
' +
'' +
'
' +
'' +
'' +
'' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'' +
'' +
'' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
Привет! Я ваш AI ассистент. Чем могу помочь?
' +
'
' + new Date().toLocaleTimeString() + '
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'
' +
'
Обрабатываю запрос...
' +
'
';
// Добавляем в 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-btn');
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-chat-messages');
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-chat-messages');
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-chat-messages');
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-chat-messages');
if (content) {
this.scrollToBottom();
}
} else {
clearInterval(interval);
}
}, speed);
}
// Метод для показа индикатора печатания
showTypingIndicator() {
const content = this.drawer.querySelector('.ai-chat-messages');
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 = `
печатает...
`;
contentDiv.appendChild(typingDiv);
// Собираем сообщение
messageDiv.appendChild(avatarDiv);
messageDiv.appendChild(contentDiv);
content.appendChild(messageDiv);
// Автоматически прокручиваем к последнему сообщению
this.scrollToBottom();
return messageDiv;
}
}
// Метод для скрытия индикатора печатания
hideTypingIndicator() {
const content = this.drawer.querySelector('.ai-chat-messages');
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-chat-messages') || 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-chat-messages');
if (content) {
// Используем requestAnimationFrame для более плавной прокрутки
requestAnimationFrame(() => {
content.scrollTop = content.scrollHeight;
// Дополнительная проверка через небольшую задержку
setTimeout(() => {
content.scrollTop = content.scrollHeight;
}, 100);
});
}
}
}