Files
DLE/frontend/src/composables/useChat.js

383 lines
19 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.

import { ref, computed, watch, onMounted } from 'vue';
import api from '../api/axios';
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
import { generateUniqueId } from '../utils/helpers';
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(getFromStorage('guestId', ''));
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 countResponse = await api.get('/api/chat/history', { params: { count_only: true } });
if (!countResponse.data.success) throw new Error('Не удалось получить количество сообщений');
totalMessages = countResponse.data.total || countResponse.data.count || 0;
console.log(`[useChat] Всего сообщений в истории: ${totalMessages}`);
} 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 response = await api.get('/api/chat/history', {
params: {
offset: effectiveOffset,
limit: messageLoading.value.limit,
},
});
if (response.data.success) {
const loadedMessages = response.data.messages || [];
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 && totalMessages > 0 && effectiveOffset > 0) {
messageLoading.value.offset = effectiveOffset + loadedMessages.length;
} else {
messageLoading.value.offset += loadedMessages.length;
}
console.log(`[useChat] Новое смещение: ${messageLoading.value.offset}`);
// Проверяем, есть ли еще сообщения для загрузки
// Используем totalMessages, если он был успешно получен
if (totalMessages >= 0) {
messageLoading.value.hasMoreMessages = messageLoading.value.offset < totalMessages;
} else {
// Если total не известен, считаем, что есть еще, если загрузили полный лимит
messageLoading.value.hasMoreMessages = loadedMessages.length === messageLoading.value.limit;
}
console.log(`[useChat] Есть еще сообщения: ${messageLoading.value.hasMoreMessages}`);
} else {
// Если сообщений не пришло, значит, больше нет
messageLoading.value.hasMoreMessages = false;
}
// Очищаем гостевые данные после успешной аутентификации и загрузки
if (authType) {
removeFromStorage('guestMessages');
removeFromStorage('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 = '/api/chat/message';
if (isGuestMessage) {
if (!guestId.value) {
guestId.value = generateUniqueId();
setToStorage('guestId', guestId.value);
}
formData.append('guestId', guestId.value);
apiUrl = '/api/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.aiMessage) {
messages.value.push({
...response.data.aiMessage,
sender_type: 'assistant', // Убедимся, что тип правильный
role: 'assistant',
});
}
// Сохраняем гостевое сообщение (если нужно)
// В текущей реализации HomeView гостевые сообщения из localstorage загружаются только при старте
// Если нужна синхронизация после отправки, логику нужно добавить/изменить
/*
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'); // Очистить при ошибке парсинга
}
}
};
// --- 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 = [];
// Гостевые данные очищаются при успешной аутентификации в loadMessages
// или если пользователь сам очистит localStorage
}
});
// --- Инициализация ---
onMounted(() => {
if (!auth.isAuthenticated.value && guestId.value) {
loadGuestMessagesFromStorage();
} else if (auth.isAuthenticated.value) {
loadMessages({ initial: true });
}
// Добавляем слушатель для возможности принудительной перезагрузки
// window.addEventListener('load-chat-history', () => loadMessages({ initial: true }));
});
// onUnmounted(() => {
// window.removeEventListener('load-chat-history', () => loadMessages({ initial: true }));
// });
return {
messages,
newMessage, // v-model
attachments, // v-model
isLoading,
messageLoading, // Содержит isLoadingHistory и hasMoreMessages
userLanguage,
hasUserSentMessage,
loadMessages,
handleSendMessage,
loadGuestMessagesFromStorage, // Экспортируем на всякий случай
};
}