ваше сообщение коммита

This commit is contained in:
2025-04-30 14:31:48 +03:00
parent 3734bea350
commit 01f96a9b80
9 changed files with 1089 additions and 1307 deletions

View File

@@ -0,0 +1,383 @@
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, // Экспортируем на всякий случай
};
}