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 Ассистент' + '' + '
' + '
' + '' + '' + '' + '' + '' + '
' + '
' + '' + '' + '' + '' + '' + '
' + '
' + '
' + '
' + '
' + '
' + '

Привет! Я ваш 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); }); } } }