Files

1139 lines
49 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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">&times;</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);
});
}
}
}