- Добавлен 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 минут)
821 lines
35 KiB
JavaScript
821 lines
35 KiB
JavaScript
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">×</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');
|
||
});
|