Files
DLE/frontend/src/composables/useChat.js
2026-03-01 22:03:48 +03:00

591 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.

/**
* 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, // Экспортируем для вызова после авторизации
};
}