1215 lines
56 KiB
JavaScript
1215 lines
56 KiB
JavaScript
class AIDrawer {
|
||
constructor() {
|
||
this.isOpen = false;
|
||
this.fontSize = 'normal';
|
||
this.avatarType = 'default';
|
||
this.sessionId = null;
|
||
this.currentEventSource = null; // Для SSE соединения
|
||
this.drawerWidth = 400; // Текущая ширина drawer
|
||
this.isResizing = false; // Флаг перетаскивания
|
||
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-resize-handle"></div>' +
|
||
'<div class="ai-drawer-header">' +
|
||
'<span>AI Ассистент</span>' +
|
||
'<button class="ai-drawer-close">×</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('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false // 24-часовой формат
|
||
}) + '</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');
|
||
this.resizeHandle = document.querySelector('.ai-drawer-resize-handle');
|
||
|
||
// Обработчики событий
|
||
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();
|
||
|
||
// Инициализируем изменение размера
|
||
this.initResize();
|
||
|
||
// Обработчик изменения размера окна - ограничиваем ширину если нужно
|
||
window.addEventListener('resize', () => {
|
||
if (this.isOpen && this.drawerWidth > window.innerWidth / 2) {
|
||
const maxWidth = window.innerWidth / 2;
|
||
this.setDrawerWidth(maxWidth);
|
||
}
|
||
});
|
||
|
||
console.log('AI Drawer: Простая инициализация завершена');
|
||
}
|
||
|
||
initResize() {
|
||
if (!this.resizeHandle || !this.drawer) return;
|
||
|
||
// Загружаем сохраненную ширину
|
||
const savedWidth = localStorage.getItem('ai-drawer-width');
|
||
if (savedWidth) {
|
||
const width = parseInt(savedWidth, 10);
|
||
const maxWidth = window.innerWidth / 2;
|
||
// Проверяем что ширина в допустимых пределах для текущего экрана
|
||
if (width >= 300 && width <= maxWidth) {
|
||
this.setDrawerWidth(width);
|
||
} else if (width > maxWidth) {
|
||
// Если сохраненная ширина больше максимума - ограничиваем
|
||
console.log('AI Drawer: Saved width', width, 'exceeds max', maxWidth, ', adjusting');
|
||
this.setDrawerWidth(maxWidth);
|
||
} else {
|
||
// Если меньше минимума - устанавливаем минимум
|
||
console.log('AI Drawer: Saved width', width, 'is less than minimum, setting to 300');
|
||
this.setDrawerWidth(300);
|
||
}
|
||
}
|
||
|
||
// Обработчики для перетаскивания
|
||
this.resizeHandle.addEventListener('mousedown', (e) => {
|
||
e.preventDefault();
|
||
this.startResize(e);
|
||
});
|
||
}
|
||
|
||
startResize(e) {
|
||
this.isResizing = true;
|
||
this.drawer.classList.add('resizing');
|
||
document.body.style.cursor = 'ew-resize';
|
||
document.body.style.userSelect = 'none';
|
||
|
||
const startX = e.clientX;
|
||
const startWidth = this.drawerWidth;
|
||
|
||
const doResize = (e) => {
|
||
if (!this.isResizing) return;
|
||
|
||
const diff = startX - e.clientX; // Инвертируем, т.к. drawer справа
|
||
const newWidth = Math.max(300, Math.min(window.innerWidth / 2, startWidth + diff));
|
||
|
||
this.setDrawerWidth(newWidth);
|
||
};
|
||
|
||
const stopResize = () => {
|
||
this.isResizing = false;
|
||
this.drawer.classList.remove('resizing');
|
||
document.body.style.cursor = '';
|
||
document.body.style.userSelect = '';
|
||
document.removeEventListener('mousemove', doResize);
|
||
document.removeEventListener('mouseup', stopResize);
|
||
|
||
// Сохраняем ширину
|
||
localStorage.setItem('ai-drawer-width', this.drawerWidth.toString());
|
||
};
|
||
|
||
document.addEventListener('mousemove', doResize);
|
||
document.addEventListener('mouseup', stopResize);
|
||
}
|
||
|
||
setDrawerWidth(width) {
|
||
if (this.drawer) {
|
||
// Ограничиваем ширину максимум до половины экрана и минимум 300px
|
||
const maxWidth = window.innerWidth / 2;
|
||
const finalWidth = Math.max(300, Math.min(maxWidth, width));
|
||
this.drawerWidth = finalWidth;
|
||
|
||
this.drawer.style.width = finalWidth + 'px';
|
||
|
||
// Обновляем margin для main-container
|
||
const mainContainer = document.querySelector('.main-container');
|
||
if (mainContainer && this.isOpen) {
|
||
mainContainer.style.setProperty('--drawer-width', finalWidth + 'px');
|
||
mainContainer.style.marginRight = finalWidth + 'px';
|
||
mainContainer.setAttribute('data-drawer-width', finalWidth);
|
||
}
|
||
}
|
||
}
|
||
|
||
toggle() {
|
||
if (this.isOpen) {
|
||
this.close();
|
||
} else {
|
||
this.open();
|
||
}
|
||
}
|
||
|
||
open() {
|
||
console.log('AI Drawer: Opening drawer');
|
||
if (this.drawer) {
|
||
// Проверяем и корректируем ширину перед открытием
|
||
const maxWidth = window.innerWidth / 2;
|
||
if (this.drawerWidth > maxWidth) {
|
||
console.log('AI Drawer: Adjusting width from', this.drawerWidth, 'to', maxWidth);
|
||
this.setDrawerWidth(maxWidth);
|
||
} else if (this.drawerWidth < 300) {
|
||
console.log('AI Drawer: Adjusting width from', this.drawerWidth, 'to 300');
|
||
this.setDrawerWidth(300);
|
||
}
|
||
|
||
// Убеждаемся что ширина применена к drawer
|
||
this.drawer.style.width = this.drawerWidth + 'px';
|
||
|
||
// Убеждаемся что drawer правильно позиционирован перед открытием
|
||
this.drawer.style.right = '0';
|
||
this.drawer.style.transform = 'translateX(0)';
|
||
|
||
this.drawer.classList.add('open');
|
||
}
|
||
|
||
document.body.classList.add('ai-drawer-open');
|
||
this.isOpen = true;
|
||
|
||
// Обновляем margin для main-container с текущей шириной
|
||
const mainContainer = document.querySelector('.main-container');
|
||
if (mainContainer) {
|
||
mainContainer.style.setProperty('--drawer-width', this.drawerWidth + 'px');
|
||
mainContainer.style.marginRight = this.drawerWidth + 'px';
|
||
mainContainer.setAttribute('data-drawer-width', this.drawerWidth);
|
||
}
|
||
|
||
// Прокручиваем вниз к последнему сообщению при открытии
|
||
const scrollToBottomOnOpen = () => {
|
||
const drawerContent = this.drawer?.querySelector('.ai-drawer-content');
|
||
const chatMessages = this.drawer?.querySelector('.ai-chat-messages');
|
||
|
||
if (drawerContent) {
|
||
const scroll = () => {
|
||
drawerContent.scrollTop = drawerContent.scrollHeight;
|
||
console.log('AI Drawer: Scrolled on open, scrollTop:', drawerContent.scrollTop, 'scrollHeight:', drawerContent.scrollHeight);
|
||
};
|
||
|
||
// Прокручиваем последнее сообщение в видимую область
|
||
if (chatMessages && chatMessages.lastElementChild) {
|
||
chatMessages.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||
}
|
||
|
||
requestAnimationFrame(() => {
|
||
scroll();
|
||
requestAnimationFrame(scroll);
|
||
});
|
||
}
|
||
};
|
||
|
||
// Прокручиваем после анимации открытия
|
||
setTimeout(scrollToBottomOnOpen, 300);
|
||
setTimeout(scrollToBottomOnOpen, 600);
|
||
|
||
// История уже загружена при инициализации страницы
|
||
// Не нужно дополнительных запросов при открытии
|
||
}
|
||
|
||
close() {
|
||
console.log('AI Drawer: Closing drawer');
|
||
if (this.drawer) {
|
||
this.drawer.classList.remove('open');
|
||
// Убеждаемся что drawer скрыт через transform
|
||
if (!this.drawer.classList.contains('open')) {
|
||
this.drawer.style.transform = 'translateX(100%)';
|
||
}
|
||
}
|
||
|
||
document.body.classList.remove('ai-drawer-open');
|
||
this.isOpen = false;
|
||
|
||
// Убираем margin у main-container
|
||
const mainContainer = document.querySelector('.main-container');
|
||
if (mainContainer) {
|
||
mainContainer.style.marginRight = '';
|
||
mainContainer.removeAttribute('data-drawer-width');
|
||
}
|
||
}
|
||
|
||
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');
|
||
}
|
||
}
|
||
|
||
// Функция для определения читаемого текста ссылки на основе URL
|
||
getLinkText(url) {
|
||
const urlLower = url.toLowerCase();
|
||
const maxLength = 60; // Максимальная длина ссылки до замены
|
||
|
||
// Если ссылка короткая, показываем её полностью
|
||
if (url.length <= maxLength) {
|
||
return url;
|
||
}
|
||
|
||
// Анализируем URL и определяем тип действия
|
||
if (urlLower.includes('download') || urlLower.includes('file') || urlLower.includes('скачать')) {
|
||
return '📥 Скачать документ';
|
||
}
|
||
|
||
if (urlLower.includes('edit') || urlLower.includes('редактир') || urlLower.includes('onlyoffice')) {
|
||
return '✏️ Открыть для редактирования';
|
||
}
|
||
|
||
if (urlLower.includes('view') || urlLower.includes('просмотр') || urlLower.includes('preview')) {
|
||
return '👁️ Открыть для просмотра';
|
||
}
|
||
|
||
if (urlLower.includes('document') || urlLower.includes('документ') || urlLower.includes('.docx') || urlLower.includes('.pdf')) {
|
||
return '📄 Открыть документ';
|
||
}
|
||
|
||
if (urlLower.includes('create') || urlLower.includes('создать')) {
|
||
return '➕ Создать документ';
|
||
}
|
||
|
||
// Общие варианты для длинных ссылок
|
||
const linkTexts = [
|
||
'🔗 Открыть ссылку',
|
||
'👉 Смотреть здесь',
|
||
'📋 Подробнее',
|
||
'🔍 Перейти к документу',
|
||
'📎 Открыть'
|
||
];
|
||
|
||
// Выбираем случайный вариант для разнообразия
|
||
return linkTexts[Math.floor(Math.random() * linkTexts.length)];
|
||
}
|
||
|
||
// Функция для преобразования URL в кликабельные ссылки
|
||
convertUrlsToLinks(text) {
|
||
if (!text) return '';
|
||
|
||
let result = text;
|
||
|
||
// ШАГ 1: Обрабатываем Markdown ссылки [текст](url)
|
||
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||
result = result.replace(markdownLinkRegex, (match, linkText, url) => {
|
||
// Проверяем, что это валидный URL
|
||
if (url.match(/^https?:\/\//i)) {
|
||
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="ai-message-link" title="${url}">${linkText}</a>`;
|
||
}
|
||
return match; // Если не URL, оставляем как есть
|
||
});
|
||
|
||
// ШАГ 2: Временно заменяем уже существующие HTML-ссылки на плейсхолдеры
|
||
const htmlLinks = [];
|
||
const htmlLinkRegex = /<a\s+[^>]*href\s*=\s*["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi;
|
||
result = result.replace(htmlLinkRegex, (match, href, linkText) => {
|
||
const placeholder = `__HTML_LINK_${htmlLinks.length}__`;
|
||
htmlLinks.push({ href, linkText, match });
|
||
return placeholder;
|
||
});
|
||
|
||
// ШАГ 3: Экранируем оставшийся HTML для безопасности
|
||
const escaped = result
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
|
||
// ШАГ 4: Восстанавливаем HTML-ссылки (плейсхолдеры не экранированы, т.к. не содержат < >)
|
||
let finalResult = escaped;
|
||
htmlLinks.forEach((link, index) => {
|
||
const placeholder = `__HTML_LINK_${index}__`;
|
||
// Используем оригинальную ссылку, но добавляем класс если его нет
|
||
let htmlLink = link.match;
|
||
if (!htmlLink.includes('class=')) {
|
||
htmlLink = htmlLink.replace('<a', '<a class="ai-message-link"');
|
||
} else if (!htmlLink.includes('ai-message-link')) {
|
||
htmlLink = htmlLink.replace('class="', 'class="ai-message-link ');
|
||
htmlLink = htmlLink.replace("class='", "class='ai-message-link ");
|
||
}
|
||
// Убеждаемся что есть target="_blank"
|
||
if (!htmlLink.includes('target=')) {
|
||
htmlLink = htmlLink.replace('<a', '<a target="_blank" rel="noopener noreferrer"');
|
||
}
|
||
// Заменяем плейсхолдер на правильный HTML (не экранированный)
|
||
finalResult = finalResult.replace(placeholder, htmlLink);
|
||
});
|
||
|
||
// ШАГ 5: Преобразуем обычные URL в ссылки (только те, что не внутри уже существующих ссылок)
|
||
const urlRegex = /(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/gi;
|
||
let urlMatches = [];
|
||
let match;
|
||
|
||
// Сначала находим все URL и проверяем их контекст
|
||
while ((match = urlRegex.exec(finalResult)) !== null) {
|
||
const url = match[0];
|
||
const offset = match.index;
|
||
const beforeMatch = finalResult.substring(0, offset);
|
||
|
||
// Проверяем, нет ли открывающего тега <a перед этим URL
|
||
const lastOpenTag = beforeMatch.lastIndexOf('<a');
|
||
const lastCloseTag = beforeMatch.lastIndexOf('</a>');
|
||
|
||
// Если есть открывающий тег <a и нет закрывающего после него - значит мы внутри ссылки
|
||
if (lastOpenTag > lastCloseTag) {
|
||
continue; // Пропускаем, это уже часть ссылки
|
||
}
|
||
|
||
// Проверяем, не является ли это частью href атрибута
|
||
if (beforeMatch.lastIndexOf('href=') > lastCloseTag) {
|
||
continue; // Пропускаем, это часть href
|
||
}
|
||
|
||
urlMatches.push({ url, offset });
|
||
}
|
||
|
||
// Заменяем URL в обратном порядке (чтобы не сбить индексы)
|
||
for (let i = urlMatches.length - 1; i >= 0; i--) {
|
||
const { url, offset } = urlMatches[i];
|
||
const linkText = this.getLinkText(url);
|
||
const linkHtml = `<a href="${url}" target="_blank" rel="noopener noreferrer" class="ai-message-link" title="${url}">${linkText}</a>`;
|
||
finalResult = finalResult.substring(0, offset) + linkHtml + finalResult.substring(offset + url.length);
|
||
}
|
||
|
||
return finalResult;
|
||
}
|
||
|
||
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');
|
||
// Преобразуем URL в кликабельные ссылки
|
||
textDiv.innerHTML = this.convertUrlsToLinks(text);
|
||
contentDiv.appendChild(textDiv);
|
||
|
||
const timeDiv = document.createElement('div');
|
||
timeDiv.className = 'ai-message-time';
|
||
if (customTime) {
|
||
// Если передано время из истории, используем его
|
||
try {
|
||
// Логируем для отладки
|
||
console.log('AI Drawer: Parsing timestamp:', customTime);
|
||
const historyTime = new Date(customTime);
|
||
// Проверяем что дата валидна
|
||
if (isNaN(historyTime.getTime())) {
|
||
// Если дата невалидна, пытаемся распарсить как строку времени (старый формат)
|
||
console.warn('AI Drawer: Invalid timestamp format:', customTime, 'Parsed as:', historyTime);
|
||
timeDiv.textContent = customTime; // Показываем как есть
|
||
} else {
|
||
// Определяем, нужно ли показывать дату
|
||
const now = new Date();
|
||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||
const messageDate = new Date(historyTime.getFullYear(), historyTime.getMonth(), historyTime.getDate());
|
||
const isToday = messageDate.getTime() === today.getTime();
|
||
|
||
let formattedTime;
|
||
if (isToday) {
|
||
// Если сообщение сегодня - показываем только время
|
||
formattedTime = historyTime.toLocaleTimeString('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false
|
||
});
|
||
} else {
|
||
// Если сообщение не сегодня - показываем дату и время
|
||
const dateStr = historyTime.toLocaleDateString('ru-RU', {
|
||
day: '2-digit',
|
||
month: '2-digit'
|
||
});
|
||
const timeStr = historyTime.toLocaleTimeString('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false
|
||
});
|
||
formattedTime = `${dateStr} ${timeStr}`;
|
||
}
|
||
|
||
console.log('AI Drawer: Successfully formatted timestamp:', customTime, '->', formattedTime);
|
||
timeDiv.textContent = formattedTime;
|
||
}
|
||
} catch (error) {
|
||
console.error('AI Drawer: Error parsing timestamp:', customTime, error);
|
||
timeDiv.textContent = customTime || new Date().toLocaleTimeString('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false // 24-часовой формат
|
||
});
|
||
}
|
||
} else {
|
||
timeDiv.textContent = new Date().toLocaleTimeString('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false // 24-часовой формат
|
||
});
|
||
}
|
||
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('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false // 24-часовой формат
|
||
});
|
||
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;
|
||
let currentText = '';
|
||
const interval = setInterval(() => {
|
||
if (index < text.length) {
|
||
currentText += text[index];
|
||
// Преобразуем URL в кликабельные ссылки по мере добавления текста
|
||
element.innerHTML = this.convertUrlsToLinks(currentText);
|
||
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);
|
||
}
|
||
});
|
||
|
||
// Прокручиваем вниз к последнему сообщению после загрузки истории
|
||
const scrollToBottom = () => {
|
||
const drawerContent = this.drawer?.querySelector('.ai-drawer-content');
|
||
const chatMessages = this.drawer?.querySelector('.ai-chat-messages');
|
||
|
||
if (drawerContent) {
|
||
// Способ 1: Прокручиваем контейнер
|
||
const scroll = () => {
|
||
drawerContent.scrollTop = drawerContent.scrollHeight;
|
||
console.log('AI Drawer: Scrolled container, scrollTop:', drawerContent.scrollTop, 'scrollHeight:', drawerContent.scrollHeight);
|
||
};
|
||
|
||
// Способ 2: Прокручиваем последнее сообщение в видимую область
|
||
if (chatMessages && chatMessages.lastElementChild) {
|
||
chatMessages.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||
console.log('AI Drawer: Scrolled last message into view');
|
||
}
|
||
|
||
// Используем requestAnimationFrame для более надежной прокрутки
|
||
requestAnimationFrame(() => {
|
||
scroll();
|
||
requestAnimationFrame(() => {
|
||
scroll();
|
||
});
|
||
});
|
||
} else {
|
||
console.warn('AI Drawer: Drawer content not found for scrolling');
|
||
}
|
||
};
|
||
|
||
// Прокручиваем с несколькими задержками для надежности
|
||
setTimeout(scrollToBottom, 100);
|
||
setTimeout(scrollToBottom, 300);
|
||
setTimeout(scrollToBottom, 600);
|
||
|
||
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');
|
||
});
|