Files
crm.clientright.ru/layouts/v7/resources/js/ai-drawer-simple.js
Fedor ac7467f0b4 Major CRM updates: AI Assistant, Court Status API, S3 integration improvements, and extensive file storage system
- Added comprehensive AI Assistant system (aiassist/ directory):
  * Vector search and embedding capabilities
  * Typebot proxy integration
  * Elastic search functionality
  * Message classification and chat history
  * MCP proxy for external integrations

- Implemented Court Status API (GetCourtStatus.php):
  * Real-time court document status checking
  * Integration with external court systems
  * Comprehensive error handling and logging

- Enhanced S3 integration:
  * Improved file backup system with metadata
  * Batch processing capabilities
  * Enhanced error logging and recovery
  * Copy operations with URL fixing

- Added Telegram contact creation API
- Improved error logging across all modules
- Enhanced callback system for AI responses
- Extensive backup file storage with timestamps
- Updated documentation and README files

- File storage improvements:
  * Thousands of backup files with proper metadata
  * Fix operations for broken file references
  * Project-specific backup and recovery systems
  * Comprehensive file integrity checking

Total: 26,461+ files added/modified including AWS SDK, vendor dependencies, and extensive backup system.
2025-10-16 11:17:21 +03:00

681 lines
28 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.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) {
// Запрос принят, начинаем polling по task_id
console.log('AI Drawer: Request accepted, task_id:', data.task_id);
this.startPolling(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);
}
}
// Метод для polling результатов
async startPolling(taskId) {
console.log('AI Drawer: Starting polling for task:', taskId);
const pollInterval = setInterval(async () => {
try {
const completed = await this.checkAIResult(taskId);
if (completed) {
clearInterval(pollInterval);
}
} catch (error) {
console.error('AI Drawer: Polling 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);
}
// Метод для проверки результата
async checkAIResult(taskId) {
console.log('AI Drawer: Checking result for task:', taskId);
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();
console.log('AI Drawer: Result check response:', data);
if (data.status === 'completed') {
// Задача завершена
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.response, false, 25);
return true; // Остановить polling
} else if (data.status === 'error') {
// Ошибка обработки
this.hideLoading();
this.hideTypingIndicator();
this.addStreamingMessage(data.error || 'Произошла ошибка при обработке запроса.', false, 25);
return true; // Остановить polling
} else {
// Задача еще обрабатывается
console.log('AI Drawer: Still processing...');
return false; // Продолжить polling
}
}
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');
});