Files
crm.clientright.ru/layouts/v7/resources/js/ai-drawer-simple.js
Fedor 1a4653298d Реализован SSE + Redis Pub/Sub для AI Drawer
- Добавлен SSE endpoint (aiassist/ai_sse.php) для real-time получения ответов от n8n
- Обновлен n8n_proxy.php: убран callback, добавлена передача Redis параметров в n8n
- Обновлен ai-drawer-simple.js: переход с polling на SSE с fallback через Redis
- Добавлен check_redis_response.php для прямого чтения из Redis кэша
- Добавлена документация: N8N_REDIS_SETUP.md, N8N_REDIS_FIX.md, AI_DRAWER_REDIS_SSE.md
- Поддержка plain text ответов от n8n (автоматическое определение формата)
- Кэширование ответов в Redis для надежности (TTL 5 минут)
2025-11-11 15:16:27 +03:00

821 lines
35 KiB
JavaScript
Raw 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.sessionId = null;
this.currentEventSource = null; // Для SSE соединения
this.init();
// Загружаем историю сразу при инициализации (при загрузке страницы)
// чтобы когда пользователь откроет drawer - история уже была готова
setTimeout(() => {
this.preloadChatHistory();
}, 2000);
}
init() {
console.log('AI Drawer: Простая инициализация начата');
// Создаем простой HTML без inline стилей
const drawerHTML =
'<button class="ai-drawer-toggle">AI</button>' +
'<div class="ai-drawer font-normal">' +
'<div class="ai-drawer-header">' +
'<span>AI Ассистент</span>' +
'<button class="ai-drawer-close">&times;</button>' +
'</div>' +
'<div class="ai-font-controls">' +
'<label>Размер шрифта:</label>' +
'<button class="font-btn" data-size="small">Мелкий</button>' +
'<button class="font-btn active" data-size="normal">Обычный</button>' +
'<button class="font-btn" data-size="large">Крупный</button>' +
'<button class="font-btn" data-size="extra-large">Очень крупный</button>' +
'</div>' +
'<div class="ai-avatar-controls">' +
'<label>Аватарка ассистента:</label>' +
'<button class="avatar-btn active" data-type="default">🤖</button>' +
'<button class="avatar-btn" data-type="friendly">😊</button>' +
'<button class="avatar-btn" data-type="helpful">💡</button>' +
'<button class="avatar-btn" data-type="smart">🧠</button>' +
'</div>' +
'<div class="ai-drawer-content">' +
'<div class="ai-chat-messages">' +
'<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 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');
// Обработчики событий
if (this.toggleBtn) {
this.toggleBtn.onclick = () => this.toggle();
}
if (this.closeBtn) {
this.closeBtn.onclick = () => this.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');
};
});
// Обработчики для кнопок управления аватаркой
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');
};
});
// Обработчики для поля ввода
if (this.sendButton && this.chatInput) {
this.sendButton.onclick = () => this.sendMessage();
this.chatInput.onkeypress = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.sendMessage();
}
};
}
// Восстанавливаем настройки
this.restoreSettings();
console.log('AI Drawer: Простая инициализация завершена');
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open() {
console.log('AI Drawer: Opening drawer');
if (this.drawer) {
this.drawer.classList.add('open');
}
document.body.classList.add('ai-drawer-open');
this.isOpen = true;
// История уже загружена при инициализации страницы
// Не нужно дополнительных запросов при открытии
}
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;
}
setFontSize(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.setItem('ai-drawer-font-size', size);
}
setAvatarType(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);
}
showLoading(message = 'Обрабатываю запрос...') {
if (this.loadingOverlay) {
const textElement = this.loadingOverlay.querySelector('div:last-child');
if (textElement) {
textElement.textContent = message;
}
this.loadingOverlay.classList.add('show');
}
}
hideLoading() {
if (this.loadingOverlay) {
this.loadingOverlay.classList.remove('show');
}
}
addMessage(text, isUser = false, customTime = null) {
console.log('AI Drawer: addMessage called with:', {text: text.substring(0, 50), isUser, customTime});
// Ищем контейнер сообщений - может быть .ai-chat-messages или .ai-drawer-content
const chatMessages = this.drawer.querySelector('.ai-chat-messages');
const drawerContent = this.drawer.querySelector('.ai-drawer-content');
const content = chatMessages || drawerContent;
console.log('AI Drawer: Container search results:', {
chatMessages: !!chatMessages,
drawerContent: !!drawerContent,
selectedContent: !!content,
drawerExists: !!this.drawer
});
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';
if (customTime) {
// Если передано время из истории, используем его
const historyTime = new Date(customTime);
timeDiv.textContent = historyTime.toLocaleTimeString();
} else {
timeDiv.textContent = new Date().toLocaleTimeString();
}
contentDiv.appendChild(timeDiv);
messageDiv.appendChild(avatarDiv);
messageDiv.appendChild(contentDiv);
content.appendChild(messageDiv);
content.scrollTop = content.scrollHeight;
console.log('AI Drawer: Message successfully added to DOM');
// Сохранение происходит автоматически в n8n, поэтому не дублируем
} else {
console.error('AI Drawer: Content container not found! Cannot add message.');
}
}
addStreamingMessage(text, isUser = false, speed = 30) {
// Ищем контейнер сообщений - может быть .ai-chat-messages или .ai-drawer-content
const content = this.drawer.querySelector('.ai-chat-messages') || this.drawer.querySelector('.ai-drawer-content');
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);
content.scrollTop = content.scrollHeight;
// Запускаем стриминг
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-drawer-content');
if (content) {
content.scrollTop = content.scrollHeight;
}
} else {
clearInterval(interval);
}
}, speed);
}
showTypingIndicator() {
// Ищем контейнер сообщений - может быть .ai-chat-messages или .ai-drawer-content
const content = this.drawer.querySelector('.ai-chat-messages') || this.drawer.querySelector('.ai-drawer-content');
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);
content.scrollTop = content.scrollHeight;
return messageDiv;
}
}
hideTypingIndicator() {
// Ищем контейнер сообщений - может быть .ai-chat-messages или .ai-drawer-content
const content = this.drawer.querySelector('.ai-chat-messages') || this.drawer.querySelector('.ai-drawer-content');
if (content) {
const typingIndicator = content.querySelector('.ai-typing-indicator');
if (typingIndicator) {
const messageDiv = typingIndicator.closest('.ai-message');
if (messageDiv) {
messageDiv.remove();
}
}
}
}
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.sendToN8N(message);
}
async sendToN8N(message) {
try {
console.log('AI Drawer: Sending to n8n:', message);
this.showTypingIndicator();
const context = this.getCurrentContext();
// Используем существующую сессию или создаем новую только один раз
if (!this.sessionId) {
this.sessionId = 'ai-drawer-session-' + Date.now();
console.log('AI Drawer: Created new session:', this.sessionId);
} else {
console.log('AI Drawer: Reusing existing session:', this.sessionId);
}
const sessionId = this.sessionId;
const response = await fetch('/aiassist/n8n_proxy.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: message,
context: context,
sessionId: sessionId
})
});
if (!response.ok) {
throw new Error(`N8N Proxy error: ${response.status}`);
}
const data = await response.json();
console.log('AI Drawer: n8n proxy response:', data);
console.log('AI Drawer: data.success =', data.success, 'type:', typeof data.success);
if (data.success && data.task_id) {
// Запрос принят, подписываемся на SSE события через Redis
console.log('AI Drawer: Request accepted, task_id:', data.task_id);
this.startSSEListener(data.task_id);
} else {
throw new Error(data.message || 'Unknown error');
}
} catch (error) {
console.error('AI Drawer: n8n error:', error);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage('Извините, произошла ошибка при обработке запроса. Попробуйте еще раз.', false, 25);
}
}
// Метод для подписки на SSE события через Redis Pub/Sub
startSSEListener(taskId) {
console.log('AI Drawer: Starting SSE listener for task:', taskId);
// Закрываем предыдущее соединение если есть
if (this.currentEventSource) {
this.currentEventSource.close();
}
// Флаг для отслеживания получения ответа
let responseReceived = false;
// Создаем новое SSE соединение
const sseUrl = `/aiassist/ai_sse.php?task_id=${encodeURIComponent(taskId)}`;
this.currentEventSource = new EventSource(sseUrl);
// Обработчик подключения
this.currentEventSource.onopen = () => {
console.log('AI Drawer: SSE connection opened');
};
// Обработчик получения ответа
this.currentEventSource.addEventListener('response', (event) => {
try {
const data = JSON.parse(event.data);
console.log('AI Drawer: Received response via SSE:', data);
if (data.data && data.data.response) {
responseReceived = true; // Отмечаем что получили ответ
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.data.response, false, 25);
this.currentEventSource.close();
this.currentEventSource = null;
}
} catch (error) {
console.error('AI Drawer: SSE response parse error:', error);
}
});
// Обработчик ошибок SSE (стандартное событие)
this.currentEventSource.onerror = (event) => {
console.error('AI Drawer: SSE connection error:', event);
console.log('AI Drawer: SSE readyState:', this.currentEventSource?.readyState);
// НЕ вызываем fallback если уже получили ответ (SSE закрывается после отправки)
if (responseReceived) {
console.log('AI Drawer: Response already received, ignoring SSE close error');
return;
}
// Fallback на polling только если SSE действительно не работает
if (this.currentEventSource && this.currentEventSource.readyState === EventSource.CLOSED) {
console.log('AI Drawer: SSE closed without response, falling back to Redis check');
this.currentEventSource.close();
this.currentEventSource = null;
// Вместо polling БД проверяем Redis напрямую
this.checkRedisDirectly(taskId);
}
};
// Обработчик кастомных ошибок от сервера
this.currentEventSource.addEventListener('error', (event) => {
try {
const data = JSON.parse(event.data);
console.error('AI Drawer: SSE error event from server:', data);
if (data.data && data.data.error) {
responseReceived = true; // Отмечаем что получили ответ (даже если ошибка)
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.data.error || 'Произошла ошибка при обработке запроса.', false, 25);
this.currentEventSource.close();
this.currentEventSource = null;
}
} catch (error) {
// Игнорируем ошибки парсинга
}
});
// Обработчик heartbeat (поддержание соединения)
this.currentEventSource.addEventListener('heartbeat', (event) => {
console.log('AI Drawer: SSE heartbeat received');
});
// Обработчик подключения
this.currentEventSource.addEventListener('connected', (event) => {
console.log('AI Drawer: SSE connected:', event.data);
});
// Таймаут на случай если SSE не работает (fallback на Redis check)
setTimeout(() => {
if (this.currentEventSource && this.currentEventSource.readyState === EventSource.CONNECTING && !responseReceived) {
console.log('AI Drawer: SSE timeout, checking Redis directly');
this.currentEventSource.close();
this.currentEventSource = null;
this.checkRedisDirectly(taskId);
}
}, 5000); // 5 секунд на подключение
// Общий таймаут ожидания ответа (5 минут)
setTimeout(() => {
if (this.currentEventSource && !responseReceived) {
this.currentEventSource.close();
this.currentEventSource = null;
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage('Время ожидания истекло. Попробуйте еще раз.', false, 25);
}
}, 300000);
}
// Метод для прямой проверки Redis (если SSE не работает)
async checkRedisDirectly(taskId) {
console.log('AI Drawer: Checking Redis directly for task:', taskId);
// Проверяем несколько раз с интервалом (на случай если ответ еще обрабатывается)
let attempts = 0;
const maxAttempts = 30; // 30 попыток = 1 минута (каждые 2 секунды)
const checkInterval = setInterval(async () => {
attempts++;
console.log(`AI Drawer: Redis check attempt ${attempts}/${maxAttempts}`);
try {
const response = await fetch(`/aiassist/check_redis_response.php?task_id=${encodeURIComponent(taskId)}`);
if (!response.ok) {
throw new Error(`Redis check failed: ${response.status}`);
}
const data = await response.json();
if (data.found && data.response) {
clearInterval(checkInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.response, false, 25);
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage('Ответ не получен. Попробуйте отправить запрос еще раз.', false, 25);
}
} catch (error) {
console.error('AI Drawer: Redis direct check error:', error);
if (attempts >= maxAttempts) {
clearInterval(checkInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage('Ошибка при получении ответа. Попробуйте еще раз.', false, 25);
}
}
}, 2000); // Проверяем каждые 2 секунды
}
// Fallback метод для polling (если SSE не работает)
async startPollingFallback(taskId) {
console.log('AI Drawer: Starting polling fallback for task:', taskId);
const pollInterval = setInterval(async () => {
try {
const response = await fetch(`/get_ai_result.php?task_id=${taskId}`);
if (!response.ok) {
throw new Error(`Result check failed: ${response.status}`);
}
const data = await response.json();
if (data.status === 'completed') {
clearInterval(pollInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.response, false, 25);
} else if (data.status === 'error') {
clearInterval(pollInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.error || 'Произошла ошибка при обработке запроса.', false, 25);
}
} catch (error) {
console.error('AI Drawer: Polling fallback error:', error);
clearInterval(pollInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage('Ошибка при получении ответа. Попробуйте еще раз.', false, 25);
}
}, 2000); // Проверяем каждые 2 секунды
// Ограничиваем время ожидания (5 минут)
setTimeout(() => {
clearInterval(pollInterval);
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage('Время ожидания истекло. Попробуйте еще раз.', false, 25);
}, 300000);
}
getCurrentContext() {
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get('record') || '';
const currentModule = urlParams.get('module') || '';
const currentView = urlParams.get('view') || '';
let userId = '';
let userName = '';
let userEmail = '';
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);
}
return {
projectId: projectId,
currentModule: currentModule,
currentView: currentView,
userId: userId,
userName: userName,
userEmail: userEmail,
projectName: projectName,
pageTitle: document.title || '',
currentDate: new Date().toLocaleDateString('ru-RU'),
url: window.location.href,
timestamp: Date.now()
};
}
async initializeChat() {
try {
console.log('AI Drawer: Initializing chat with context');
const context = this.getCurrentContext();
let contextMessage = 'Привет! Я готов к работе. ';
if (context.projectName) {
contextMessage += `Сейчас я работаю с записью "${context.projectName}" в модуле ${context.currentModule}. `;
} else if (context.currentModule) {
contextMessage += `Сейчас я работаю в модуле ${context.currentModule}. `;
}
contextMessage += 'Чем могу помочь?';
this.showLoading('🤖 Инициализирую ассистента...');
await this.sendToN8N(contextMessage);
} catch (error) {
console.error('AI Drawer: Chat initialization error:', error);
this.hideLoading();
this.addStreamingMessage('Привет! Я готов к работе. Чем могу помочь?', false, 25);
}
}
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');
}
});
}
}
// Метод для предзагрузки истории чата через n8n вебхук
async preloadChatHistory() {
try {
console.log('AI Drawer: Preloading chat history from n8n webhook');
const context = this.getCurrentContext();
console.log('AI Drawer: Context for history:', context);
// Отправляем запрос на получение истории через локальный эндпоинт
const sessionId = 'ai-drawer-session-' + (context.projectId || 'default') + '-' + (context.userId || '1');
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) {
console.log('AI Drawer: Chat history not available from n8n:', response.status);
return;
}
const data = await response.json();
console.log('AI Drawer: Chat history loaded from get_chat_history.php:', data);
// Локальный эндпоинт возвращает объект с полем history
if (data.success && Array.isArray(data.history) && data.history.length > 0) {
console.log('AI Drawer: Found', data.history.length, 'history messages');
// Очищаем текущие сообщения
this.clearMessages();
// Добавляем историю
data.history.forEach((msg, index) => {
try {
console.log(`AI Drawer: Adding history message ${index + 1}:`, msg.type, msg.message.substring(0, 30) + '...');
this.addMessage(msg.message, msg.type === 'user', msg.timestamp);
console.log(`AI Drawer: Successfully added message ${index + 1}`);
} catch (error) {
console.error('AI Drawer: Error adding history message:', error, msg);
}
});
console.log('AI Drawer: Chat history restored -', data.history.length, 'messages');
} else {
console.log('AI Drawer: No chat history found. Response:', data);
}
} catch (error) {
console.error('AI Drawer: Error loading chat history from n8n:', error);
}
}
// Метод для очистки сообщений
clearMessages() {
// Ищем контейнер сообщений - может быть .ai-chat-messages или .ai-drawer-content
const messagesContainer = this.drawer.querySelector('.ai-chat-messages') || this.drawer.querySelector('.ai-drawer-content');
if (messagesContainer) {
// Удаляем все сообщения
const messages = messagesContainer.querySelectorAll('.ai-message');
messages.forEach(msg => msg.remove());
console.log('AI Drawer: Messages cleared from', messagesContainer.className);
} else {
console.log('AI Drawer: Messages container not found');
}
}
// Метод для обновления истории (при смене страницы)
async refreshPreloadedHistory() {
console.log('AI Drawer: Refreshing preloaded history');
await this.preloadChatHistory();
}
// Сохранение сообщений происходит автоматически в n8n
// Поэтому метод saveMessageToHistory не нужен
}
// Инициализация AI Drawer при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
console.log('AI Drawer: DOM loaded, initializing...');
window.aiDrawer = new AIDrawer();
console.log('AI Drawer: Initialized successfully');
});