Описание изменений

This commit is contained in:
2025-03-19 17:18:03 +03:00
parent 04d027054d
commit 55e4d81c95
75 changed files with 2103 additions and 4861 deletions

View File

@@ -5,29 +5,39 @@
<div class="auth-section" v-if="!auth.isAuthenticated">
<h3>Венчурный фонд и поставщик программного обеспечения</h3>
</div>
<div class="chat-container">
<div class="chat-header">
<WalletConnection
:onWalletAuth="handleWalletAuth"
:isAuthenticated="auth.isAuthenticated"
/>
<div class="user-info" v-if="auth.isAuthenticated">
<!-- Используем тот же компонент, что и в сообщениях -->
<div v-if="!auth.isAuthenticated" class="auth-buttons">
<button class="auth-btn wallet-btn" @click="handleWalletAuth">
<span class="auth-icon">👛</span> Подключить кошелек
</button>
</div>
<div v-else class="wallet-info">
<span>{{ truncateAddress(auth.address) }}</span>
<button class="disconnect-btn" @click="disconnectWallet">
Отключить кошелек
</button>
</div>
</div>
<!-- Кнопка загрузки предыдущих сообщений -->
<div v-if="hasMoreMessages" class="load-more-container">
<button @click="loadMoreMessages" class="load-more-btn" :disabled="isLoadingMore">
<div v-if="auth.isAuthenticated && hasMoreMessages" class="load-more">
<button @click="loadMoreMessages" :disabled="isLoadingMore">
{{ isLoadingMore ? 'Загрузка...' : 'Показать предыдущие сообщения' }}
</button>
</div>
<div class="chat-messages" ref="messagesContainer">
<div v-if="isLoadingMore" class="loading">
Загрузка...
</div>
<div v-for="message in messages" :key="message.id" :class="['message', message.role === 'assistant' ? 'ai-message' : 'user-message']">
<div class="message-content">
{{ message.content }}
</div>
<!-- Кнопки аутентификации -->
<div v-if="message.showAuthButtons && !auth.isAuthenticated" class="auth-buttons">
<button class="auth-btn wallet-btn" @click="handleWalletAuth">
@@ -39,32 +49,32 @@
<button class="auth-btn email-btn" @click="handleEmailAuth">
<span class="auth-icon"></span> Подключить Email
</button>
</div>
</div>
<!-- Email форма -->
<div v-if="showEmailForm" class="auth-form">
<input
<input
v-model="emailInput"
type="email"
placeholder="Введите ваш email"
type="email"
placeholder="Введите ваш email"
class="auth-input"
/>
/>
<button @click="submitEmail" class="auth-btn">
Отправить код
</button>
</div>
</button>
</div>
<!-- Форма верификации email -->
<div v-if="showEmailVerification" class="auth-form">
<input
<input
v-model="emailCode"
type="text"
type="text"
placeholder="Введите код из email"
class="auth-input"
/>
/>
<button @click="verifyEmailCode" class="auth-btn">
Подтвердить
</button>
</button>
</div>
<!-- Telegram верификация -->
@@ -77,9 +87,9 @@
/>
<button @click="verifyTelegramCode" class="auth-btn">
Подтвердить
</button>
</div>
</button>
</div>
<div v-if="emailError" class="error-message">
{{ emailError }}
</div>
@@ -106,17 +116,18 @@
</template>
<script setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue';
import { ref, computed, onMounted, watch, nextTick, onBeforeUnmount } from 'vue';
import { useAuthStore } from '../stores/auth';
import WalletConnection from '../components/WalletConnection.vue';
import TelegramConnect from '../components/TelegramConnect.vue';
import axios from '../api/axios';
import { connectWithWallet } from '../utils/wallet';
import WalletConnection from '../components/identity/WalletConnection.vue';
import TelegramConnect from '../components/identity/TelegramConnect.vue';
import api from '../api/axios';
import { connectWithWallet } from '../services/wallet';
console.log('HomeView.vue: Version with chat loaded');
const auth = useAuthStore();
const messages = ref([]);
const guestMessages = ref([]);
const newMessage = ref('');
const isLoading = ref(false);
const messagesContainer = ref(null);
@@ -124,7 +135,6 @@ const userLanguage = ref('ru');
const email = ref('');
const isValidEmail = ref(true);
const hasShownAuthMessage = ref(false);
const guestMessages = ref([]);
const hasShownAuthOptions = ref(false);
// Email аутентификация
@@ -144,8 +154,10 @@ const emailError = ref('');
const PAGE_SIZE = 2; // Показываем только последнее сообщение и ответ
const allMessages = ref([]); // Все загруженные сообщения
const currentPage = ref(1); // Текущая страница
const hasMoreMessages = ref(false); // Есть ли еще сообщения
const hasMoreMessages = ref(true); // Есть ли еще сообщения
const isLoadingMore = ref(false); // Загружаются ли дополнительные сообщения
const offset = ref(0);
const limit = ref(20);
// Вычисляемое свойство для отображаемых сообщений
const displayedMessages = computed(() => {
@@ -153,48 +165,30 @@ const displayedMessages = computed(() => {
return allMessages.value.slice(startIndex);
});
// Функция загрузки истории чата
const loadChatHistory = async () => {
try {
if (!auth.isAuthenticated || !auth.userId) {
return;
}
// Функция для сокращения адреса кошелька
const truncateAddress = (address) => {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
const response = await axios.get('/api/chat/history', {
headers: { Authorization: `Bearer ${auth.address}` },
params: { limit: PAGE_SIZE, offset: 0 }
});
if (response.data.success) {
messages.value = response.data.messages.map(msg => ({
id: msg.id,
content: msg.content,
role: msg.role || (msg.sender_type === 'assistant' ? 'assistant' : 'user'),
timestamp: msg.created_at,
showAuthOptions: false
}));
hasMoreMessages.value = response.data.total > PAGE_SIZE;
await nextTick();
scrollToBottom();
}
} catch (error) {
console.error('Error loading chat history:', error);
// Функция прокрутки к последнему сообщению
const scrollToBottom = () => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
};
// Функция загрузки дополнительных сообщений
// Загрузка сообщений
const loadMoreMessages = async () => {
if (isLoadingMore.value) return;
if (!auth.isAuthenticated) return;
try {
isLoadingMore.value = true;
const offset = messages.value.length;
const response = await axios.get('/api/chat/history', {
headers: { Authorization: `Bearer ${auth.address}` },
params: { limit: PAGE_SIZE, offset }
const response = await api.get('/api/chat/history', {
params: {
limit: limit.value,
offset: offset.value
}
});
if (response.data.success) {
@@ -205,83 +199,80 @@ const loadMoreMessages = async () => {
timestamp: msg.created_at,
showAuthOptions: false
}));
messages.value = [...newMessages, ...messages.value];
messages.value = [...messages.value, ...newMessages];
hasMoreMessages.value = response.data.total > messages.value.length;
offset.value += newMessages.length;
}
} catch (error) {
console.error('Error loading more messages:', error);
console.error('Error loading chat history:', error);
} finally {
isLoadingMore.value = false;
}
};
// Функция прокрутки к последнему сообщению
const scrollToBottom = () => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
};
// Инициализация при монтировании
onMounted(async () => {
console.log('HomeView.vue: onMounted called');
console.log('Auth state:', auth.isAuthenticated);
// Определяем язык
const cyrillicPattern = /[а-яА-ЯёЁ]/;
userLanguage.value = cyrillicPattern.test(newMessage.value) ? 'ru' : 'en';
console.log('Detected language:', userLanguage.value);
// Если пользователь уже аутентифицирован, загружаем историю
if (auth.isAuthenticated && auth.userId) {
console.log('User authenticated, loading chat history...');
await loadChatHistory();
// Загружаем сообщения при изменении аутентификации
watch(() => auth.isAuthenticated, async (newValue) => {
if (newValue) {
messages.value = [];
offset.value = 0;
hasMoreMessages.value = true;
try {
// Сначала загружаем историю из messages
await loadMoreMessages();
// Связываем гостевые сообщения (копируем из guest_messages в messages)
await api.post('/api/chat/link-guest-messages');
console.log('Guest messages linked to authenticated user');
// Перезагружаем сообщения, чтобы получить все, включая перенесенные
messages.value = [];
offset.value = 0;
await loadMoreMessages();
await nextTick();
scrollToBottom();
} catch (linkError) {
console.error('Error linking guest messages:', linkError);
}
} else {
messages.value = [];
offset.value = 0;
hasMoreMessages.value = true;
}
});
// Наблюдатель за изменением состояния аутентификации
watch(() => auth.isAuthenticated, async (newValue, oldValue) => {
console.log('Auth state changed in HomeView:', newValue);
if (newValue && auth.userId) {
// Пользователь только что аутентифицировался
await loadChatHistory();
} else {
// Пользователь вышел из системы
messages.value = []; // Очищаем историю сообщений
hasMoreMessages.value = false; // Сбрасываем флаг наличия дополнительных сообщений
console.log('Chat history cleared after logout');
}
}, { immediate: true });
// Функция для подключения кошелька
const handleWalletAuth = async () => {
try {
const result = await connectWithWallet();
if (result.success) {
console.log('Wallet auth result:', result);
// Сохраняем гостевые сообщения перед очисткой
const guestMessages = [...messages.value];
messages.value = [];
offset.value = 0;
hasMoreMessages.value = true;
// Обновляем состояние аутентификации
auth.setAuth({
authenticated: true,
isAuthenticated: true,
userId: result.userId,
address: result.address,
isAdmin: result.isAdmin,
authType: 'wallet'
});
// Добавляем задержку для синхронизации сессии
await new Promise(resolve => setTimeout(resolve, 1000));
// Загружаем историю чата
await loadChatHistory();
try {
await api.post('/api/chat/link-guest-messages');
console.log('Guest messages linked to authenticated user');
await loadMoreMessages();
const filteredGuestMessages = guestMessages
.filter(msg => !msg.showAuthButtons)
.reverse();
messages.value = [...messages.value, ...filteredGuestMessages];
await nextTick();
scrollToBottom();
} catch (linkError) {
console.error('Error linking guest messages:', linkError);
}
}
return result;
} catch (error) {
console.error('Error connecting wallet:', error);
throw error;
}
};
@@ -295,7 +286,7 @@ const saveGuestMessagesToServer = async () => {
// Отправляем каждое сообщение на сервер
for (const msg of userMessages) {
await axios.post('/api/chat/message', {
await api.post('/api/chat/message', {
message: msg.content,
language: userLanguage.value
});
@@ -311,7 +302,7 @@ const saveGuestMessagesToServer = async () => {
async function connectTelegram() {
try {
// Отправляем запрос на получение ссылки для авторизации через Telegram
const response = await axios.get('/api/auth/telegram', {
const response = await api.get('/api/auth/telegram', {
withCredentials: true
});
@@ -376,7 +367,7 @@ async function requestEmailCode() {
// Функция проверки кода
const verifyEmailCode = async () => {
try {
const response = await axios.post('/api/auth/email/verify-code', {
const response = await api.post('/api/auth/email/verify-code', {
email: emailInput.value,
code: emailCode.value
});
@@ -387,7 +378,7 @@ const verifyEmailCode = async () => {
emailError.value = '';
// Загружаем историю чата после успешной аутентификации
await loadChatHistory();
await loadMoreMessages();
} else {
emailError.value = response.data.error || 'Неверный код';
}
@@ -404,12 +395,6 @@ function cancelEmailVerification() {
emailErrorMessage.value = '';
}
// Добавьте эту функцию в <script setup>
const formatAddress = (address) => {
if (!address) return '';
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
@@ -438,101 +423,73 @@ const formatTime = (timestamp) => {
};
// Функция для отправки сообщения
const handleMessage = async (messageText) => {
if (!messageText.trim() || isLoading.value) return;
console.log('Handling message:', messageText);
isLoading.value = true;
const handleMessage = async (text) => {
try {
const messageContent = text.trim();
if (!messageContent) return;
newMessage.value = '';
isLoading.value = true;
if (!auth.isAuthenticated) {
await sendGuestMessage(messageText);
// Сохраняем в таблицу guest_messages
const response = await api.post('/api/chat/guest-message', {
message: messageContent,
language: userLanguage.value
});
if (response.data.success) {
const userMessage = {
id: response.data.messageId,
content: messageContent,
role: 'user',
timestamp: new Date().toISOString(),
showAuthButtons: false
};
messages.value.push(userMessage);
messages.value.push({
id: Date.now() + 1,
content: 'Для получения ответа от ассистента, пожалуйста, авторизуйтесь одним из способов:',
role: 'assistant',
timestamp: new Date().toISOString(),
showAuthButtons: true
});
}
} else {
await sendMessage(messageText);
// Для авторизованного пользователя сохраняем в messages
const response = await api.post('/api/chat/message', {
message: messageContent,
language: userLanguage.value
});
if (response.data.success) {
const message = {
id: response.data.messageId,
content: messageContent,
role: 'user',
timestamp: new Date().toISOString(),
hasResponse: true
};
messages.value.push(message);
const aiMessage = {
id: response.data.aiMessageId,
content: response.data.message,
role: 'assistant',
timestamp: new Date().toISOString()
};
messages.value.push(aiMessage);
}
}
} catch (error) {
console.error('Error handling message:', error);
console.error('Error sending message:', error);
messages.value.push({
id: Date.now(),
content: 'Произошла ошибка при отправке сообщения.',
role: 'assistant',
timestamp: new Date().toISOString()
});
} finally {
newMessage.value = '';
isLoading.value = false;
}
};
// Функция для отправки сообщения аутентифицированного пользователя
const sendMessage = async (messageText) => {
try {
const userMessage = {
id: Date.now(),
content: messageText,
role: 'user',
timestamp: new Date().toISOString()
};
messages.value.push(userMessage);
const response = await axios.post('/api/chat/message', {
message: messageText,
language: userLanguage.value
});
if (response.data.success) {
messages.value.push({
id: Date.now() + 1,
content: response.data.message,
role: 'assistant',
timestamp: new Date().toISOString()
});
}
} catch (error) {
console.error('Error sending message:', error);
}
};
// Функция для отправки гостевого сообщения
const sendGuestMessage = async (messageText) => {
try {
// Добавляем сообщение пользователя
const userMessage = {
id: Date.now(),
content: messageText,
role: 'user',
timestamp: new Date().toISOString(),
showAuthButtons: false
};
messages.value.push(userMessage);
// Очищаем поле ввода
newMessage.value = '';
// Сохраняем сообщение на сервере без получения ответа от Ollama
await axios.post('/api/chat/guest-message', {
message: messageText,
language: userLanguage.value
});
// Добавляем сообщение с кнопками аутентификации
messages.value.push({
id: Date.now() + 1,
content: 'Для получения ответа, пожалуйста, авторизуйтесь одним из способов:',
role: 'assistant',
timestamp: new Date().toISOString(),
showAuthButtons: true
});
} catch (error) {
console.error('Error sending guest message:', error);
messages.value.push({
id: Date.now() + 2,
content: 'Произошла ошибка. Пожалуйста, попробуйте позже.',
role: 'assistant',
timestamp: new Date().toISOString(),
showAuthButtons: true
});
} finally {
isLoading.value = false;
}
@@ -554,7 +511,7 @@ const handleEmailAuth = async () => {
// Функция отправки email
const submitEmail = async () => {
try {
const response = await axios.post('/api/auth/email/request', {
const response = await api.post('/api/auth/email/request', {
email: emailInput.value
});
@@ -573,7 +530,7 @@ const submitEmail = async () => {
// Функция верификации кода Telegram
const verifyTelegramCode = async () => {
try {
const response = await axios.post('/api/auth/telegram/verify', {
const response = await api.post('/api/auth/telegram/verify', {
code: telegramCode.value
});
@@ -602,7 +559,7 @@ const verifyTelegramCode = async () => {
// Загружаем историю чата после небольшой задержки
setTimeout(async () => {
await loadChatHistory();
await loadMoreMessages();
}, 100);
} else {
messages.value.push({
@@ -622,6 +579,43 @@ const verifyTelegramCode = async () => {
});
}
};
const disconnectWallet = async () => {
try {
await auth.disconnect();
messages.value = [];
offset.value = 0;
hasMoreMessages.value = true;
} catch (error) {
console.error('Error disconnecting wallet:', error);
}
};
// Обработка прокрутки
const handleScroll = async () => {
const element = messagesContainer.value;
if (
!isLoadingMore.value &&
hasMoreMessages.value &&
element.scrollTop === 0
) {
await loadMoreMessages();
}
};
onMounted(() => {
// Добавляем слушатель прокрутки
if (messagesContainer.value) {
messagesContainer.value.addEventListener('scroll', handleScroll);
}
});
onBeforeUnmount(() => {
// Удаляем слушатель
if (messagesContainer.value) {
messagesContainer.value.removeEventListener('scroll', handleScroll);
}
});
</script>
<style scoped>
@@ -665,64 +659,49 @@ h1 {
}
.chat-header {
padding: 1rem;
border-bottom: 1px solid #ddd;
background-color: #f8f9fa;
display: flex;
justify-content: space-between;
justify-content: flex-end;
align-items: center;
padding: 10px 20px;
background-color: #f0f0f0;
border-bottom: 1px solid #ccc;
}
/* Адаптивный заголовок чата */
@media (max-width: 768px) {
.chat-header {
padding: 8px 12px;
}
.chat-header h2 {
font-size: 1.2rem;
margin: 0;
}
}
.user-info {
.wallet-info {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.9rem;
gap: 1rem;
}
/* Адаптивная информация о пользователе */
@media (max-width: 768px) {
.user-info {
font-size: 0.7rem;
gap: 5px;
}
.user-info span {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.logout-btn {
padding: 5px 10px;
background-color: #f44336;
.disconnect-btn {
padding: 0.5rem 1rem;
background-color: #ff4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
/* Адаптивная кнопка выхода */
@media (max-width: 768px) {
.logout-btn {
padding: 4px 8px;
font-size: 0.8rem;
}
.disconnect-btn:hover {
background-color: #cc0000;
}
.load-more {
text-align: center;
padding: 1rem;
}
.load-more button {
padding: 0.5rem 1rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.load-more button:hover {
background-color: #0056b3;
}
.chat-messages {
@@ -915,32 +894,27 @@ h1 {
}
.auth-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1rem;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
border: none;
width: 100%;
font-weight: 500;
transition: opacity 0.2s;
box-sizing: border-box;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.auth-btn:hover {
opacity: 0.9;
.wallet-btn {
background-color: #4a5568;
color: white;
}
.auth-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
.wallet-btn:hover {
background-color: #2d3748;
}
.auth-icon {
margin-right: 0.75rem;
font-size: 1.2rem;
font-size: 16px;
}
.telegram-btn {
@@ -1080,4 +1054,48 @@ h1 {
background-color: #cbd5e0;
cursor: not-allowed;
}
.wallet-section {
margin-top: 20px;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #f9f9f9;
}
.wallet-info {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.disconnect-btn {
padding: 0.5rem 1rem;
background-color: #ff4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.disconnect-btn:hover {
background-color: #cc0000;
}
.chat-history {
height: 60vh;
overflow-y: auto;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 1rem;
}
/* Добавим индикатор загрузки */
.loading {
text-align: center;
padding: 1rem;
color: #666;
}
</style>