/** * Copyright (c) 2024-2026 Тарабанов Александр Викторович * All rights reserved. * * This software is proprietary and confidential. * Unauthorized copying, modification, or distribution is prohibited. * * For licensing inquiries: info@hb3-accelerator.com * Website: https://hb3-accelerator.com * GitHub: https://github.com/VC-HB3-Accelerator */ import { ref, computed, watch, onMounted, onUnmounted } from 'vue'; import api from '../api/axios'; import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage'; import { generateUniqueId } from '../utils/helpers'; import websocketModule from '../services/websocketService'; import { getPublicMessages } from '../services/messagesService'; const { websocketService } = websocketModule; function initGuestId() { let id = getFromStorage('guestId', ''); if (!id) { id = generateUniqueId(); setToStorage('guestId', id); } return id; } export function useChat(auth) { const messages = ref([]); const newMessage = ref(''); const attachments = ref([]); // Теперь это массив File объектов const userLanguage = ref('ru'); const isLoading = ref(false); // Общая загрузка (например, при отправке) const hasUserSentMessage = ref(getFromStorage('hasUserSentMessage') === true); const messageLoading = ref({ isLoadingHistory: false, // Загрузка истории hasMoreMessages: false, offset: 0, limit: 30, isHistoryLoadingInProgress: false, // Флаг для предотвращения параллельных запросов истории isLinkingGuest: false, // Флаг для процесса связывания гостевых сообщений (пока не используется активно) }); const guestId = ref(initGuestId()); // Сохраняем ссылки на callback функции WebSocket для правильной отписки const wsCallbacks = { chatMessage: null, conversationUpdated: null, connected: null, disconnected: null, error: null }; const shouldLoadHistory = computed(() => { return auth.isAuthenticated.value || !!guestId.value; }); // --- Загрузка истории --- const loadMessages = async (options = {}) => { const { silent = false, initial = false, authType = null } = options; if (messageLoading.value.isHistoryLoadingInProgress) { // console.warn('[useChat] Загрузка истории уже идет, пропуск.'); return; } messageLoading.value.isHistoryLoadingInProgress = true; // Если initial=true, сбрасываем offset и hasMoreMessages if (initial) { // console.log('[useChat] Начальная загрузка истории...'); messageLoading.value.offset = 0; messageLoading.value.hasMoreMessages = false; messages.value = []; // Очищаем текущие сообщения перед начальной загрузкой } // Не загружаем больше, если уже грузим или больше нет if (!initial && (!messageLoading.value.hasMoreMessages || messageLoading.value.isLoadingHistory)) { messageLoading.value.isHistoryLoadingInProgress = false; return; } messageLoading.value.isLoadingHistory = true; if (!silent && initial) isLoading.value = true; // Показываем общий лоадер только при начальной загрузке // console.log( // `[useChat] Загрузка истории сообщений (initial: ${initial}, authType: ${authType}, offset: ${messageLoading.value.offset})...` // ); try { // --- Логика ожидания привязки гостя (упрощенная) --- // TODO: Рассмотреть более надежный механизм, если это необходимо if (authType) { // console.log(`[useChat] Ожидание после ${authType} аутентификации...`); await new Promise((resolve) => setTimeout(resolve, 1500)); // Увеличена задержка // console.log('[useChat] Ожидание завершено, продолжаем загрузку истории.'); } // --- Конец логики ожидания --- // Определяем, нужно ли запрашивать count let totalMessages = -1; if (initial || messageLoading.value.offset === 0) { try { // Получаем количество личных сообщений с ИИ const personalCountResponse = await api.get('/chat/history', { params: { count_only: true } }); const personalCount = personalCountResponse.data.success ? (personalCountResponse.data.total || 0) : 0; // Получаем количество публичных сообщений const publicCountResponse = await api.get('/messages/public', { params: { count_only: true } }); const publicCount = publicCountResponse.data.success ? (publicCountResponse.data.total || 0) : 0; totalMessages = personalCount + publicCount; // console.log(`[useChat] Всего сообщений в истории: ${totalMessages} (личные: ${personalCount}, публичные: ${publicCount})`); } catch(countError) { // console.error('[useChat] Ошибка получения количества сообщений:', countError); // Не прерываем выполнение, попробуем загрузить без total } } let effectiveOffset = messageLoading.value.offset; // Если это первая загрузка и мы знаем total, рассчитаем смещение для последних сообщений if (initial && totalMessages > 0 && totalMessages > messageLoading.value.limit) { effectiveOffset = Math.max(0, totalMessages - messageLoading.value.limit); // console.log(`[useChat] Рассчитано начальное смещение: ${effectiveOffset}`); } // Загружаем личные сообщения с ИИ const personalResponse = await api.get('/chat/history', { params: { offset: effectiveOffset, limit: messageLoading.value.limit } }); // Загружаем публичные сообщения от других пользователей const publicResponse = await api.get('/messages/public', { params: { offset: 0, limit: 50 } }); // Объединяем сообщения let allMessages = []; if (personalResponse.data.success && personalResponse.data.messages) { allMessages = [...allMessages, ...personalResponse.data.messages]; } if (publicResponse.data.success && publicResponse.data.messages) { allMessages = [...allMessages, ...publicResponse.data.messages]; } // Сортируем по времени создания allMessages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); const response = { data: { success: true, messages: allMessages, total: allMessages.length } }; if (response.data.success && response.data.messages) { const loadedMessages = response.data.messages; const totalFromResponse = response.data.total; // console.log(`[useChat] Загружено ${loadedMessages.length} сообщений.`); if (loadedMessages.length > 0) { // Добавляем к существующим (в начало для истории, в конец для начальной загрузки) if (initial) { messages.value = loadedMessages; } else { messages.value = [...loadedMessages, ...messages.value]; } // Обновляем смещение для следующей загрузки // Если загружали последние, offset = total - limit + loaded if (initial && totalFromResponse > 0 && effectiveOffset > 0) { messageLoading.value.offset = effectiveOffset + loadedMessages.length; } else { messageLoading.value.offset += loadedMessages.length; } // console.log(`[useChat] Новое смещение: ${messageLoading.value.offset}`); // Проверяем, есть ли еще сообщения для загрузки // Используем totalFromResponse из нового API if (totalFromResponse >= 0) { messageLoading.value.hasMoreMessages = messageLoading.value.offset < totalFromResponse; } else { // Если total не известен, используем hasMore из ответа messageLoading.value.hasMoreMessages = response.data.hasMore || false; } // console.log(`[useChat] Есть еще сообщения: ${messageLoading.value.hasMoreMessages}`); } else { // Если сообщений не пришло, значит, больше нет messageLoading.value.hasMoreMessages = false; } // Очищаем гостевые данные после успешной аутентификации и загрузки if (authType) { removeFromStorage('guestMessages'); // removeFromStorage('guestId'); // Удаление guestId теперь только после успешного связывания guestId.value = ''; } // Считаем, что пользователь отправлял сообщение, если история не пуста if (messages.value.length > 0) { hasUserSentMessage.value = true; setToStorage('hasUserSentMessage', true); } } else { // console.error('[useChat] API вернул ошибку при загрузке истории:', response.data.error); messageLoading.value.hasMoreMessages = false; // Считаем, что больше нет при ошибке } } catch (error) { // console.error('[useChat] Ошибка загрузки истории сообщений:', error); messageLoading.value.hasMoreMessages = false; // Считаем, что больше нет при ошибке } finally { messageLoading.value.isLoadingHistory = false; messageLoading.value.isHistoryLoadingInProgress = false; if (initial) isLoading.value = false; } }; // --- Отправка сообщения --- const handleSendMessage = async (payload) => { // --- НАЧАЛО ДОБАВЛЕННЫХ ЛОГОВ --- // console.log('[useChat] handleSendMessage called. Payload:', payload); // console.log('[useChat] Current auth state:', { // isAuthenticated: auth.isAuthenticated.value, // userId: auth.userId.value, // authType: auth.authType.value, // }); // --- КОНЕЦ ДОБАВЛЕННЫХ ЛОГОВ --- const { message: text, attachments: files } = payload; // files - массив File объектов const userMessageContent = text.trim(); // Проверка на пустое сообщение (если нет ни текста, ни файлов) if (!userMessageContent && (!files || files.length === 0)) { // console.warn('[useChat] Попытка отправить пустое сообщение.'); return; } isLoading.value = true; const tempId = generateUniqueId(); const isGuestMessage = !auth.isAuthenticated.value; // Создаем локальное сообщение для отображения const userMessage = { id: tempId, content: userMessageContent || `[${files.length} вложений]`, // Отображение для UI sender_type: 'user', role: 'user', isLocal: true, isGuest: isGuestMessage, timestamp: new Date().toISOString(), // Генерируем инфо для отображения в Message.vue (без File объектов) attachments: files ? files.map(f => ({ originalname: f.name, size: f.size, mimetype: f.type, // url: URL.createObjectURL(f) // Можно создать временный URL для превью, если Message.vue его использует })) : [], hasError: false }; messages.value.push(userMessage); // Очистка ввода происходит в ChatInterface // newMessage.value = ''; // attachments.value = []; try { const formData = new FormData(); formData.append('message', userMessageContent); formData.append('language', userLanguage.value); if (files && files.length > 0) { files.forEach((file) => { formData.append('attachments', file, file.name); }); } let apiUrl = '/chat/message'; if (isGuestMessage) { if (!guestId.value) { guestId.value = initGuestId(); setToStorage('guestId', guestId.value); } formData.append('guestId', guestId.value); apiUrl = '/chat/guest-message'; } const response = await api.post(apiUrl, formData, { headers: { // Content-Type устанавливается браузером для FormData } }); const userMsgIndex = messages.value.findIndex((m) => m.id === tempId); if (response.data.success) { // console.log('[useChat] Сообщение успешно отправлено:', response.data); // Обновляем локальное сообщение данными с сервера if (userMsgIndex !== -1) { const serverUserMessage = response.data.userMessage || { id: response.data.messageId }; messages.value[userMsgIndex].id = serverUserMessage.id || tempId; // Используем серверный ID messages.value[userMsgIndex].isLocal = false; messages.value[userMsgIndex].timestamp = serverUserMessage.created_at || new Date().toISOString(); // Опционально: обновить content/attachments с сервера, если они отличаются } // Добавляем ответ ИИ, если есть // Системное сообщение о согласиях уже включено в ответ ИИ if (response.data.aiResponse) { messages.value.push({ id: `ai_${Date.now()}`, content: response.data.aiResponse.response || response.data.aiResponse, sender_type: 'assistant', role: 'assistant', timestamp: new Date().toISOString(), isLocal: false, // Добавляем информацию о согласиях, если есть consentRequired: response.data.consentRequired || false, missingConsents: response.data.missingConsents || [], consentDocuments: response.data.consentDocuments || [], autoConsentOnReply: response.data.autoConsentOnReply || false }); } // Добавляем системное сообщение для гостя (только если нет согласия, чтобы не дублировать) if (isGuestMessage && response.data.systemMessage && !response.data.consentRequired) { messages.value.push({ id: `system-${Date.now()}`, content: response.data.systemMessage, sender_type: 'system', role: 'system', timestamp: new Date().toISOString(), isSystem: true, telegramBotUrl: response.data.telegramBotUrl, supportEmail: response.data.supportEmail }); } // Сохраняем гостевое сообщение (если нужно) if (isGuestMessage) { try { const storedMessages = getFromStorage('guestMessages', []); // Добавляем сообщение пользователя (с серверным ID) storedMessages.push({ id: messages.value[userMsgIndex].id, content: userMessageContent, sender_type: 'user', role: 'user', isGuest: true, timestamp: messages.value[userMsgIndex].timestamp, attachmentsInfo: messages.value[userMsgIndex].attachments // Сохраняем инфо о файлах }); setToStorage('guestMessages', storedMessages); } catch (storageError) { // console.error('[useChat] Ошибка сохранения гостевого сообщения в localStorage:', storageError); } } hasUserSentMessage.value = true; setToStorage('hasUserSentMessage', true); } else { throw new Error(response.data.error || 'Ошибка отправки сообщения от API'); } } catch (error) { // console.error('[useChat] Ошибка отправки сообщения:', error); const userMsgIndex = messages.value.findIndex((m) => m.id === tempId); if (userMsgIndex !== -1) { messages.value[userMsgIndex].hasError = true; messages.value[userMsgIndex].isLocal = false; // Убираем статус "отправка" } // Добавляем системное сообщение об ошибке messages.value.push({ id: `error-${Date.now()}`, content: 'Произошла ошибка при отправке сообщения. Пожалуйста, попробуйте еще раз.', sender_type: 'system', role: 'system', timestamp: new Date().toISOString(), }); } finally { isLoading.value = false; } }; // --- Управление гостевыми сообщениями --- const loadGuestMessagesFromStorage = () => { if (!auth.isAuthenticated.value && guestId.value) { try { const storedMessages = getFromStorage('guestMessages'); if (storedMessages && Array.isArray(storedMessages) && storedMessages.length > 0) { // console.log(`[useChat] Найдено ${storedMessages.length} сохраненных гостевых сообщений`); // Добавляем только если текущий список пуст (чтобы не дублировать при HMR) if(messages.value.length === 0) { messages.value = storedMessages.map(m => ({ ...m, isGuest: true })); // Помечаем как гостевые hasUserSentMessage.value = true; } } } catch (e) { // console.error('[useChat] Ошибка загрузки гостевых сообщений из localStorage:', e); removeFromStorage('guestMessages'); // Очистить при ошибке парсинга } } }; // --- Связывание гостевых сообщений после аутентификации --- const linkGuestMessagesAfterAuth = async () => { if (!guestId.value) return; try { const response = await api.post('/chat/process-guest', { guestId: guestId.value }); if (response.data.success && response.data.conversationId) { // Можно сразу загрузить историю по этому диалогу, если нужно await loadMessages({ initial: true }); // Удаляем guestId только после успешного связывания removeFromStorage('guestId'); guestId.value = ''; } } catch (error) { // console.error('[useChat] Ошибка связывания гостевых сообщений:', error); } }; // --- Watchers --- // Сортировка сообщений при изменении watch(messages, (newMessages) => { // Сортируем только если массив изменился if (newMessages.length > 1) { messages.value.sort((a, b) => { const dateA = new Date(a.timestamp || a.created_at || 0); const dateB = new Date(b.timestamp || b.created_at || 0); return dateA - dateB; }); } }, { deep: false }); // deep: false т.к. нас интересует только добавление/удаление элементов // Сброс чата при выходе пользователя watch(() => auth.isAuthenticated.value, (isAuth, wasAuth) => { if (!isAuth && wasAuth) { // Если пользователь разлогинился // console.log('[useChat] Пользователь вышел, сброс состояния чата.'); messages.value = []; messageLoading.value.offset = 0; messageLoading.value.hasMoreMessages = false; hasUserSentMessage.value = false; newMessage.value = ''; attachments.value = []; // Отключаем WebSocket cleanupWebSocket(); // Гостевые данные очищаются при успешной аутентификации в loadMessages // или если пользователь сам очистит localStorage } else if (isAuth && !wasAuth) { // Если пользователь вошел // console.log('[useChat] Пользователь вошел, подключаем WebSocket.'); // Отложенное подключение, чтобы дождаться загрузки данных пользователя setTimeout(() => setupChatWebSocket(), 100); } }); // Отслеживаем загрузку данных пользователя для подключения WebSocket watch(() => auth.user?.value, (newUser, oldUser) => { if (newUser && newUser.id && auth.isAuthenticated.value) { // console.log('[useChat] Данные пользователя загружены, подключаем WebSocket:', newUser.id); setupChatWebSocket(); } }, { immediate: false }); // --- WebSocket для real-time сообщений --- function setupChatWebSocket() { // Подключаемся к WebSocket только если пользователь аутентифицирован if (auth.isAuthenticated.value && auth.user && auth.user.value && auth.user.value.id) { // console.log('[useChat] Подключение к WebSocket для пользователя:', auth.user.value.id); websocketService.connect(auth.user.value.id); // Создаем и сохраняем callback функции wsCallbacks.chatMessage = (message) => { // console.log('[useChat] Получено новое сообщение через WebSocket:', message); // Проверяем, что сообщение не дублируется const existingMessage = messages.value.find(m => m.id === message.id); if (!existingMessage) { messages.value.push(message); } }; wsCallbacks.conversationUpdated = (conversationId) => { // console.log('[useChat] Обновление диалога через WebSocket:', conversationId); // Можно добавить логику обновления списка диалогов }; wsCallbacks.connected = () => { // console.log('[useChat] WebSocket подключен'); }; wsCallbacks.disconnected = () => { // console.log('[useChat] WebSocket отключен'); }; wsCallbacks.error = (error) => { // console.error('[useChat] WebSocket ошибка:', error); }; // Подписываемся на события websocketService.on('chat-message', wsCallbacks.chatMessage); websocketService.on('conversation-updated', wsCallbacks.conversationUpdated); websocketService.on('connected', wsCallbacks.connected); websocketService.on('disconnected', wsCallbacks.disconnected); websocketService.on('error', wsCallbacks.error); } else { // console.log('[useChat] WebSocket не подключен: пользователь не аутентифицирован или данные не загружены'); } } function cleanupWebSocket() { // Отписываемся от всех событий, передавая те же callback функции if (websocketService) { if (wsCallbacks.chatMessage) { websocketService.off('chat-message', wsCallbacks.chatMessage); } if (wsCallbacks.conversationUpdated) { websocketService.off('conversation-updated', wsCallbacks.conversationUpdated); } if (wsCallbacks.connected) { websocketService.off('connected', wsCallbacks.connected); } if (wsCallbacks.disconnected) { websocketService.off('disconnected', wsCallbacks.disconnected); } if (wsCallbacks.error) { websocketService.off('error', wsCallbacks.error); } websocketService.disconnect(); // Очищаем ссылки на callback функции Object.keys(wsCallbacks).forEach(key => { wsCallbacks[key] = null; }); } } // --- Инициализация --- onMounted(() => { if (!auth.isAuthenticated.value && guestId.value) { loadGuestMessagesFromStorage(); } else if (auth.isAuthenticated.value) { loadMessages({ initial: true }); } // Подключаем WebSocket если пользователь уже аутентифицирован setupChatWebSocket(); // Логика обновления данных централизована в useAuth.js }); onUnmounted(() => { cleanupWebSocket(); }); // Подписываемся на централизованные события очистки и обновления данных window.addEventListener('clear-application-data', () => { console.log('[useChat] Clearing chat data'); // Очищаем данные при выходе из системы messages.value = []; }); window.addEventListener('refresh-application-data', () => { console.log('[useChat] Refreshing chat data'); loadMessages({ initial: true }); // Обновляем данные при входе в систему }); return { messages, newMessage, // v-model attachments, // v-model isLoading, messageLoading, // Содержит isLoadingHistory и hasMoreMessages userLanguage, hasUserSentMessage, loadMessages, handleSendMessage, loadGuestMessagesFromStorage, // Экспортируем на всякий случай linkGuestMessagesAfterAuth, // Экспортируем для вызова после авторизации }; }