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

This commit is contained in:
2025-03-18 16:35:13 +03:00
parent 4ebea0118c
commit 479fbdb618
15 changed files with 1636 additions and 1073 deletions

View File

@@ -1,4 +1,5 @@
import axios from 'axios';
import { useAuthStore } from '../stores/auth';
// Создаем экземпляр axios с базовым URL
const instance = axios.create({
@@ -13,11 +14,22 @@ const instance = axios.create({
instance.interceptors.request.use(
(config) => {
console.log('Axios interceptor running');
const address = localStorage.getItem('walletAddress');
if (address) {
console.log('Adding Authorization header in interceptor:', `Bearer ${address}`);
config.headers.Authorization = `Bearer ${address}`;
const authStore = useAuthStore();
// Логируем параметры запроса
console.log('Request parameters:', config);
// Если уже есть заголовок Authorization, не перезаписываем его
if (config.headers.Authorization) {
return config;
}
// Если пользователь аутентифицирован и есть адрес кошелька
if (authStore.isAuthenticated && authStore.address) {
console.log('Adding Authorization header:', `Bearer ${authStore.address}`);
config.headers.Authorization = `Bearer ${authStore.address}`;
}
return config;
},
(error) => {
@@ -25,4 +37,36 @@ instance.interceptors.request.use(
}
);
// Добавляем перехватчик для обработки ответов
instance.interceptors.response.use(
(response) => {
console.log('Response from server:', response.data);
return response;
},
(error) => {
// Проверяем, что это действительно ошибка авторизации
if (error.response?.status === 401 &&
!error.config.url.includes('/auth/') &&
!error.config.url.includes('/verify') &&
!error.config.url.includes('/chat/history')) { // Не очищаем при ошибке загрузки истории
console.log('Auth error, clearing state');
const auth = useAuthStore();
auth.disconnect();
}
return Promise.reject(error);
}
);
// Пример функции для отправки гостевого сообщения на сервер
const sendGuestMessageToServer = async (messageText) => {
try {
await axios.post('/api/chat/guest-message', {
message: messageText,
language: userLanguage.value
});
} catch (error) {
console.error('Ошибка при отправке гостевого сообщения на сервер:', error);
}
};
export default instance;

View File

@@ -1,108 +1,54 @@
<template>
<div class="telegram-auth">
<div v-if="!isAuthenticating">
<a :href="telegramBotLink" target="_blank" class="telegram-btn" @click="startAuth">
<span class="auth-icon">📱</span> Подключить Telegram
</a>
</div>
<button v-if="!showVerification" class="auth-btn telegram-btn" @click="startTelegramAuth">
<span class="auth-icon">📱</span> Подключить Telegram
</button>
<div v-else class="auth-progress">
<p>Для завершения авторизации:</p>
<ol>
<li>Перейдите в Telegram-бота <strong>@{{ botUsername }}</strong></li>
<li>Если бот не открылся автоматически, скопируйте и отправьте ему команду:</li>
</ol>
<div class="auth-code">
/auth {{ authToken }}
</div>
<button class="copy-btn" @click="copyAuthCommand">Копировать команду</button>
<div class="auth-actions">
<button class="cancel-btn" @click="cancelAuth">Отмена</button>
<button class="check-btn" @click="checkAuthStatus">Проверить статус</button>
</div>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<div v-else class="verification-form">
<input
type="text"
v-model="verificationCode"
placeholder="Введите код из Telegram"
/>
<button class="auth-btn verify-btn" @click="verifyCode">Подтвердить</button>
<button class="auth-btn cancel-btn" @click="cancelVerification">Отмена</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref } from 'vue';
import { useAuthStore } from '../stores/auth';
import axios from '../api/axios';
const auth = useAuthStore();
const showVerification = ref(false);
const verificationCode = ref('');
const isAuthenticating = ref(false);
const authToken = ref('');
const botUsername = ref(process.env.VUE_APP_TELEGRAM_BOT_USERNAME || 'HB3_Accelerator_Bot');
const errorMessage = ref('');
const checkInterval = ref(null);
const startTelegramAuth = () => {
// Открываем Telegram бота в новом окне
window.open('https://t.me/your_bot_username', '_blank');
showVerification.value = true;
};
// Формируем ссылку на бота с параметром авторизации
const telegramBotLink = computed(() => {
// Возвращаем ссылку только если есть токен
if (!authToken.value) return `https://t.me/${botUsername.value}`;
return `https://t.me/${botUsername.value}?start=auth_${authToken.value}`;
});
async function startAuth() {
const verifyCode = async () => {
try {
// Сначала запрашиваем токен
const response = await auth.createTelegramAuthToken();
const response = await axios.post('/api/auth/telegram/verify', {
code: verificationCode.value
});
if (response.success) {
authToken.value = response.token;
// Теперь можно включить режим авторизации
isAuthenticating.value = true;
// И запустить проверку
checkInterval.value = setInterval(checkAuthStatus, 3000);
// Открываем Telegram
console.log(`Открывается ссылка на Telegram: ${telegramBotLink.value}`);
window.open(telegramBotLink.value, '_blank');
} else {
errorMessage.value = response.error || 'Не удалось начать авторизацию';
if (response.data.success) {
auth.setTelegramAuth(response.data);
}
} catch (error) {
console.error('Error starting Telegram auth:', error);
errorMessage.value = 'Ошибка при инициализации авторизации';
console.error('Error verifying Telegram code:', error);
}
}
};
async function checkAuthStatus() {
try {
const response = await auth.checkTelegramAuthStatus(authToken.value);
if (response.success && response.authenticated) {
// Авторизация успешна, очищаем интервал и состояние
clearInterval(checkInterval.value);
isAuthenticating.value = false;
// Здесь можно добавить дополнительные действия после успешной авторизации
}
} catch (error) {
console.error('Error checking auth status:', error);
}
}
function cancelAuth() {
clearInterval(checkInterval.value);
isAuthenticating.value = false;
authToken.value = '';
errorMessage.value = '';
}
function copyAuthCommand() {
const command = `/auth ${authToken.value}`;
navigator.clipboard.writeText(command);
// Можно добавить уведомление о копировании
}
const cancelVerification = () => {
showVerification.value = false;
verificationCode.value = '';
};
</script>
<style scoped>

View File

@@ -5,7 +5,7 @@
</div>
<div v-if="!authStore.isAuthenticated">
<button @click="handleConnectWallet" class="connect-button" :disabled="loading">
<button @click="connectWallet" class="connect-button" :disabled="loading">
<div v-if="loading" class="spinner"></div>
{{ loading ? 'Подключение...' : 'Подключить кошелек' }}
</button>
@@ -19,7 +19,7 @@
<script setup>
import { ref, onMounted } from 'vue';
import { connectWallet } from '../utils/wallet';
import { connectWithWallet } from '../utils/wallet';
import { useAuthStore } from '../stores/auth';
import { useRouter } from 'vue-router';
@@ -29,6 +29,17 @@ const loading = ref(false);
const error = ref('');
const isConnecting = ref(false);
const props = defineProps({
onWalletAuth: {
type: Function,
required: true
},
isAuthenticated: {
type: Boolean,
required: true
}
});
// Форматирование адреса кошелька
const formatAddress = (address) => {
if (!address) return '';
@@ -36,29 +47,17 @@ const formatAddress = (address) => {
};
// Функция для подключения кошелька
const handleConnectWallet = async () => {
console.log('Нажата кнопка "Подключить кошелек"');
isConnecting.value = true;
const connectWallet = async () => {
loading.value = true;
error.value = '';
try {
const result = await connectWallet();
console.log('Результат подключения:', result);
if (result.success) {
authStore.isAuthenticated = true;
authStore.user = { address: result.address };
authStore.isAdmin = result.isAdmin;
authStore.authType = result.authType;
router.push({ name: 'home' });
} else {
error.value = result.error || 'Ошибка подключения кошелька';
}
await props.onWalletAuth();
} catch (err) {
console.error('Ошибка при подключении кошелька:', err);
error.value = 'Ошибка подключения кошелька';
} finally {
isConnecting.value = false;
loading.value = false;
}
};
@@ -112,7 +111,22 @@ onMounted(async () => {
// Функция для отключения кошелька
const disconnectWallet = async () => {
await authStore.logout();
try {
// Сначала отключаем MetaMask
if (window.ethereum) {
try {
// Просто очищаем слушатели событий
window.ethereum.removeAllListeners();
} catch (error) {
console.error('Error disconnecting MetaMask:', error);
}
}
// Затем выполняем выход из системы
await authStore.disconnect(router);
} catch (error) {
console.error('Error disconnecting wallet:', error);
}
};
</script>

View File

@@ -1,41 +1,46 @@
<template>
<div class="conversation-list">
<div class="list-header">
<h3>Диалоги</h3>
<button @click="createNewConversation" class="new-conversation-btn">
<span>+</span> Новый диалог
</button>
</div>
<div v-if="authStore.isAuthenticated">
<div class="conversation-list">
<div class="list-header">
<h3>Диалоги</h3>
<button @click="createNewConversation" class="new-conversation-btn">
<span>+</span> Новый диалог
</button>
</div>
<div v-if="loading" class="loading">Загрузка диалогов...</div>
<div v-if="loading" class="loading">Загрузка диалогов...</div>
<div v-else-if="conversations.length === 0" class="empty-list">
<p>У вас пока нет диалогов.</p>
<p>Создайте новый диалог, чтобы начать общение с ИИ-ассистентом.</p>
</div>
<div v-else-if="conversations.length === 0" class="empty-list">
<p>У вас пока нет диалогов.</p>
<p>Создайте новый диалог, чтобы начать общение с ИИ-ассистентом.</p>
</div>
<div v-else class="conversations">
<div
v-for="conversation in conversations"
:key="conversation.conversation_id"
:class="[
'conversation-item',
{ active: selectedConversationId === conversation.conversation_id },
]"
@click="selectConversation(conversation.conversation_id)"
>
<div class="conversation-title">{{ conversation.title }}</div>
<div class="conversation-meta">
<span class="message-count">{{ conversation.message_count }} сообщений</span>
<span class="time">{{ formatTime(conversation.last_activity) }}</span>
<div v-else class="conversations">
<div
v-for="conversation in conversations"
:key="conversation.conversation_id"
:class="[
'conversation-item',
{ active: selectedConversationId === conversation.conversation_id },
]"
@click="selectConversation(conversation.conversation_id)"
>
<div class="conversation-title">{{ conversation.title }}</div>
<div class="conversation-meta">
<span class="message-count">{{ conversation.message_count }} сообщений</span>
<span class="time">{{ formatTime(conversation.last_activity) }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-else class="connect-wallet-prompt">
<p>Подключите кошелек для просмотра бесед</p>
</div>
</template>
<script setup>
import { ref, onMounted, computed, defineEmits } from 'vue';
import { ref, onMounted, computed, defineEmits, watch } from 'vue';
import { useAuthStore } from '../../stores/auth';
import axios from 'axios';
@@ -46,6 +51,14 @@ const conversations = ref([]);
const loading = ref(true);
const selectedConversationId = ref(null);
// Следим за изменением статуса аутентификации
watch(() => authStore.isAuthenticated, (isAuthenticated) => {
if (!isAuthenticated) {
conversations.value = []; // Очищаем список бесед при отключении
selectedConversationId.value = null;
}
});
// Загрузка списка диалогов
const fetchConversations = async () => {
try {
@@ -222,4 +235,10 @@ defineExpose({
.time {
font-size: 0.8rem;
}
.connect-wallet-prompt {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@@ -44,15 +44,40 @@ const handleEnter = (event) => {
// Отправка сообщения
const sendMessage = async () => {
if (!message.value.trim() || sending.value) return;
const messageText = message.value.trim();
if (!messageText) return;
const userMessage = {
id: Date.now(),
content: messageText,
role: auth.isAuthenticated ? 'user' : 'guest',
timestamp: new Date().toISOString()
};
messages.value.push(userMessage);
try {
sending.value = true;
// Логируем параметры запроса
console.log('Sending message to Ollama:', {
message: messageText,
language: userLanguage.value
});
const response = await axios.post(
`/api/messages/conversations/${props.conversationId}/messages`,
{ content: message.value }
);
const response = await axios.post('/api/chat/message', {
message: messageText,
language: userLanguage.value
});
// Логируем ответ от Ollama
console.log('Response from Ollama:', response.data);
// Обработка ответа
messages.value.push({
id: Date.now() + 1,
content: response.data.message,
role: 'assistant',
timestamp: new Date().toISOString()
});
// Очищаем поле ввода
message.value = '';
@@ -65,7 +90,7 @@ const sendMessage = async () => {
// Уведомляем родительский компонент о новых сообщениях
emit('message-sent', [response.data.userMessage, response.data.aiMessage]);
} catch (error) {
console.error('Error sending message:', error);
console.error('Ошибка при отправке сообщения:', error);
} finally {
sending.value = false;
}
@@ -81,6 +106,61 @@ defineExpose({
resetInput,
focus: () => textareaRef.value?.focus(),
});
const sendGuestMessage = async (messageText) => {
if (!messageText.trim()) return;
const userMessage = {
id: Date.now(),
content: messageText,
role: 'user',
timestamp: new Date().toISOString(),
isGuest: true
};
// Добавляем сообщение пользователя в локальную историю
messages.value.push(userMessage);
// Сохраняем сообщение в массиве гостевых сообщений
guestMessages.value.push(userMessage);
// Сохраняем гостевые сообщения в localStorage
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Очищаем поле ввода
newMessage.value = '';
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
// Устанавливаем состояние загрузки
isLoading.value = true;
// Вместо отправки запроса к Ollama, отправляем сообщение с кнопками для аутентификации
const authMessage = {
id: Date.now() + 1,
content: 'Чтобы продолжить, пожалуйста, аутентифицируйтесь.',
role: 'assistant',
timestamp: new Date().toISOString(),
isGuest: true,
showAuthOptions: true // Указываем, что нужно показать кнопки аутентификации
};
messages.value.push(authMessage);
guestMessages.value.push(authMessage);
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
isLoading.value = false;
};
</script>
<style scoped>

View File

@@ -1,28 +1,34 @@
<template>
<div class="message-thread" ref="threadContainer">
<div v-if="loading" class="loading">Загрузка сообщений...</div>
<div v-if="authStore.isAuthenticated">
<div class="message-thread" ref="threadContainer">
<div v-if="loading" class="loading">Загрузка сообщений...</div>
<div v-else-if="messages.length === 0" class="empty-thread">
<p>Нет сообщений. Начните диалог, отправив сообщение.</p>
</div>
<div v-else-if="messages.length === 0" class="empty-thread">
<p>Нет сообщений. Начните диалог, отправив сообщение.</p>
</div>
<div v-else class="messages">
<div v-for="message in messages" :key="message.id" :class="['message', message.sender_type]">
<div class="message-content">{{ message.content }}</div>
<div class="message-meta">
<span class="time">{{ formatTime(message.created_at) }}</span>
<span v-if="message.channel" class="channel">
{{ channelName(message.channel) }}
</span>
<div v-else class="messages">
<div v-for="message in messages" :key="message.id" :class="['message', message.sender_type]">
<div class="message-content">{{ message.content }}</div>
<div class="message-meta">
<span class="time">{{ formatTime(message.created_at) }}</span>
<span v-if="message.channel" class="channel">
{{ channelName(message.channel) }}
</span>
</div>
</div>
</div>
</div>
</div>
<div v-else class="connect-wallet-prompt">
<p>Пожалуйста, подключите кошелек для просмотра сообщений</p>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick, defineExpose } from 'vue';
import axios from 'axios';
import { useAuthStore } from '@/stores/auth'
const props = defineProps({
conversationId: {
@@ -34,6 +40,7 @@ const props = defineProps({
const messages = ref([]);
const loading = ref(true);
const threadContainer = ref(null);
const authStore = useAuthStore()
// Загрузка сообщений диалога
const fetchMessages = async () => {
@@ -104,6 +111,13 @@ watch(
}
);
// Следим за изменением статуса аутентификации
watch(() => authStore.isAuthenticated, (isAuthenticated) => {
if (!isAuthenticated) {
messages.value = []; // Очищаем сообщения при отключении
}
});
// Загрузка сообщений при монтировании компонента
onMounted(() => {
if (props.conversationId) {
@@ -191,4 +205,10 @@ defineExpose({
border-radius: 3px;
background-color: #f0f0f0;
}
.connect-wallet-prompt {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@@ -1,18 +1,37 @@
import { defineStore } from 'pinia';
import axios from '../api/axios';
const loadAuthState = () => {
const savedAuth = localStorage.getItem('auth');
if (savedAuth) {
try {
return JSON.parse(savedAuth);
} catch (e) {
console.error('Error parsing saved auth state:', e);
}
}
return null;
};
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
isAuthenticated: false,
isAdmin: false,
authType: null,
identities: {},
loading: false,
error: null,
messages: [],
address: null
}),
state: () => {
const savedState = loadAuthState();
return {
user: null,
isAuthenticated: savedState?.isAuthenticated || false,
isAdmin: savedState?.isAdmin || false,
authType: savedState?.authType || null,
identities: {},
loading: false,
error: null,
messages: [],
address: null,
wallet: null,
telegramId: savedState?.telegramId || null,
email: null,
userId: savedState?.userId || null
};
},
actions: {
async connectWallet(address, signature, message) {
@@ -421,6 +440,99 @@ export const useAuthStore = defineStore('auth', {
console.error('Error checking Telegram auth status:', error);
return { success: false, error: 'Ошибка проверки статуса' };
}
},
async disconnect(router) {
// Проверяем, действительно ли нужно выходить
if (!this.isAuthenticated) {
console.log('Already logged out, skipping disconnect');
return;
}
try {
// Сначала пробуем очистить сессию на сервере
await axios.post('/api/auth/clear-session');
await axios.post('/api/auth/logout');
// Очищаем состояние только после успешного выхода
this.clearState();
if (router) router.push('/');
} catch (error) {
console.error('Error during logout:', error);
}
},
// Выносим очистку состояния в отдельный метод
clearState() {
this.isAuthenticated = false;
this.wallet = null;
this.messages = [];
this.user = null;
this.address = null;
this.isAdmin = false;
this.authType = null;
this.identities = {};
this.telegramId = null;
this.userId = null;
// Очищаем локальное хранилище
localStorage.removeItem('wallet');
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('walletAddress');
localStorage.removeItem('auth');
},
async setWalletAuth(authData) {
this.isAuthenticated = authData.authenticated;
this.address = authData.address;
this.isAdmin = authData.isAdmin;
},
async setTelegramAuth(authData) {
this.telegramId = authData.telegramId;
// Проверяем баланс токенов если есть подключенный кошелек
if (this.address) {
await this.checkTokenBalance();
}
},
async checkTokenBalance() {
try {
const response = await axios.get(`/api/auth/check-tokens?address=${this.address}`);
this.isAdmin = response.data.isAdmin;
} catch (error) {
console.error('Error checking token balance:', error);
}
},
setAuth(authData) {
console.log('Setting auth state:', authData);
// Обновляем все поля состояния
this.isAuthenticated = authData.authenticated || authData.isAuthenticated;
this.userId = authData.userId;
this.isAdmin = authData.isAdmin;
this.authType = authData.authType;
this.address = authData.address;
// Сохраняем состояние в localStorage
const stateToSave = {
isAuthenticated: this.isAuthenticated,
userId: this.userId,
isAdmin: this.isAdmin,
authType: this.authType,
address: this.address
};
localStorage.setItem('auth', JSON.stringify(stateToSave));
console.log('Auth state updated:', {
isAuthenticated: this.isAuthenticated,
userId: this.userId,
authType: this.authType,
address: this.address
});
}
}
});

View File

@@ -1,43 +1,38 @@
import { ethers } from 'ethers';
import axios from '../api/axios';
import { useAuthStore } from '../stores/auth';
// Функция для подключения кошелька
async function connectWallet() {
// Переименовываем функцию для соответствия импорту
export async function connectWithWallet() {
try {
// Проверяем, доступен ли MetaMask
if (!window.ethereum) {
throw new Error('MetaMask не установлен');
}
// Запрашиваем доступ к аккаунтам
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const address = accounts[0];
// Получаем nonce от сервера
const nonceResponse = await axios.get(`/api/auth/nonce?address=${address}`, {
withCredentials: true
});
// Запрашиваем доступ к кошельку
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
// Получаем nonce для подписи
const nonceResponse = await axios.get(`/api/auth/nonce?address=${address}`);
const nonce = nonceResponse.data.nonce;
// Создаем сообщение для подписи
// Формируем сообщение для подписи
const message = `Подпишите это сообщение для аутентификации в DApp for Business. Nonce: ${nonce}`;
// Запрашиваем подпись
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, address]
});
// Отправляем подпись на сервер для верификации
// Подписываем сообщение
const signature = await signer.signMessage(message);
// Верифицируем подпись на сервере
const response = await axios.post('/api/auth/verify', {
address,
signature,
message
}, {
withCredentials: true
message: nonce
});
console.log('Успешно подключен:', response.data);
console.log('Wallet verification response:', response.data);
// Обновляем состояние в хранилище auth
const authStore = useAuthStore();
@@ -47,7 +42,7 @@ async function connectWallet() {
address: response.data.address
};
authStore.isAdmin = response.data.isAdmin;
authStore.authType = response.data.authType;
authStore.authType = 'wallet';
// Сохраняем адрес кошелька в локальном хранилище
localStorage.setItem('walletAddress', address);
@@ -55,13 +50,14 @@ async function connectWallet() {
return {
success: true,
authenticated: response.data.authenticated,
userId: response.data.userId,
address: response.data.address,
isAdmin: response.data.isAdmin,
authType: response.data.authType
authType: 'wallet'
};
} catch (error) {
console.error('Ошибка при подключении кошелька:', error);
return { success: false, error: error.message || 'Ошибка подключения кошелька' };
console.error('Error connecting wallet:', error);
return { success: false, error: error.message };
}
}
@@ -92,5 +88,3 @@ async function disconnectWallet() {
throw error;
}
}
export { connectWallet, disconnectWallet };

View File

@@ -7,60 +7,81 @@
</div>
<div class="chat-container">
<div class="chat-header">
<WalletConnection />
<WalletConnection
:onWalletAuth="handleWalletAuth"
:isAuthenticated="auth.isAuthenticated"
/>
<div class="user-info" v-if="auth.isAuthenticated">
</div>
</div>
<!-- Кнопка загрузки предыдущих сообщений -->
<div v-if="hasMoreMessages" class="load-more-container">
<button @click="loadMoreMessages" class="load-more-btn" :disabled="isLoadingMore">
{{ isLoadingMore ? 'Загрузка...' : 'Показать предыдущие сообщения' }}
</button>
</div>
<div class="chat-messages" ref="messagesContainer">
<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">
<span class="auth-icon">👛</span> Подключить кошелек
</button>
<button class="auth-btn telegram-btn" @click="handleTelegramAuth">
<span class="auth-icon">📱</span> Подключить Telegram
</button>
<button class="auth-btn email-btn" @click="handleEmailAuth">
<span class="auth-icon"></span> Подключить Email
</button>
</div>
<!-- Опции аутентификации -->
<div v-if="!auth.isAuthenticated && message.role === 'assistant' && !hasShownAuthOptions.value" class="auth-options">
<div class="auth-option">
<WalletConnection />
</div>
<div class="auth-option">
<TelegramConnect />
</div>
<!-- Email аутентификация: первый шаг - запрос кода -->
<div v-if="!showEmailVerification" class="auth-option email-option">
<input
type="email"
v-model="email"
placeholder="Введите ваш email"
class="email-input"
/>
<button class="auth-btn email-btn" @click="requestEmailCode" :disabled="!isValidEmail">
<span class="auth-icon"></span> Подключить Email
</button>
</div>
<!-- Email аутентификация: второй шаг - ввод кода -->
<div v-else class="auth-option email-verification">
<p>Код подтверждения отправлен на {{ email }}</p>
<input
type="text"
v-model="emailVerificationCode"
placeholder="Введите код подтверждения"
class="verification-input"
/>
<div class="email-verification-actions">
<button class="auth-btn email-btn" @click="verifyEmailCode">
<span class="auth-icon"></span> Подтвердить
</button>
<button class="auth-btn cancel-btn" @click="cancelEmailVerification">
Отмена
</button>
</div>
</div>
<div v-if="emailErrorMessage" class="error-message">{{ emailErrorMessage }}</div>
<!-- Email форма -->
<div v-if="showEmailForm" class="auth-form">
<input
v-model="emailInput"
type="email"
placeholder="Введите ваш email"
class="auth-input"
/>
<button @click="submitEmail" class="auth-btn">
Отправить код
</button>
</div>
<!-- Форма верификации email -->
<div v-if="showEmailVerification" class="auth-form">
<input
v-model="emailCode"
type="text"
placeholder="Введите код из email"
class="auth-input"
/>
<button @click="verifyEmailCode" class="auth-btn">
Подтвердить
</button>
</div>
<!-- Telegram верификация -->
<div v-if="showTelegramVerification" class="auth-form">
<input
v-model="telegramCode"
type="text"
placeholder="Введите код из Telegram"
class="auth-input"
/>
<button @click="verifyTelegramCode" class="auth-btn">
Подтвердить
</button>
</div>
<div v-if="emailError" class="error-message">
{{ emailError }}
</div>
<div class="message-time">
@@ -72,11 +93,11 @@
<div class="chat-input">
<textarea
v-model="newMessage"
@keydown.enter.prevent="sendMessage"
@keydown.enter.prevent="handleMessage(newMessage)"
placeholder="Введите сообщение..."
:disabled="isLoading"
></textarea>
<button @click="sendMessage" :disabled="isLoading || !newMessage.trim()">
<button @click="handleMessage(newMessage)" :disabled="isLoading || !newMessage.trim()">
{{ isLoading ? 'Отправка...' : 'Отправить' }}
</button>
</div>
@@ -90,6 +111,7 @@ 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';
console.log('HomeView.vue: Version with chat loaded');
@@ -110,168 +132,156 @@ const emailVerificationCode = ref('');
const showEmailVerification = ref(false);
const emailErrorMessage = ref('');
// Простая функция для выхода
const logout = async () => {
await auth.logout();
messages.value = [];
// Добавляем состояния для форм верификации
const showTelegramVerification = ref(false);
const showEmailForm = ref(false);
const telegramCode = ref('');
const emailInput = ref('');
const emailCode = ref('');
const emailError = ref('');
// Добавляем состояния для пагинации
const PAGE_SIZE = 2; // Показываем только последнее сообщение и ответ
const allMessages = ref([]); // Все загруженные сообщения
const currentPage = ref(1); // Текущая страница
const hasMoreMessages = ref(false); // Есть ли еще сообщения
const isLoadingMore = ref(false); // Загружаются ли дополнительные сообщения
// Вычисляемое свойство для отображаемых сообщений
const displayedMessages = computed(() => {
const startIndex = Math.max(allMessages.value.length - (PAGE_SIZE * currentPage.value), 0);
return allMessages.value.slice(startIndex);
});
// Функция загрузки истории чата
const loadChatHistory = async () => {
try {
if (!auth.isAuthenticated || !auth.userId) {
return;
}
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 formatTime = (timestamp) => {
if (!timestamp) return '';
// Функция загрузки дополнительных сообщений
const loadMoreMessages = async () => {
if (isLoadingMore.value) return;
try {
const date = new Date(timestamp);
isLoadingMore.value = true;
const offset = messages.value.length;
// Проверяем, является ли дата валидной
if (isNaN(date.getTime())) {
console.warn('Invalid timestamp:', timestamp);
return '';
}
// Форматируем дату с указанием дня, месяца, года и времени
return date.toLocaleString([], {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
const response = await axios.get('/api/chat/history', {
headers: { Authorization: `Bearer ${auth.address}` },
params: { limit: PAGE_SIZE, offset }
});
if (response.data.success) {
const newMessages = 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
}));
messages.value = [...newMessages, ...messages.value];
hasMoreMessages.value = response.data.total > messages.value.length;
}
} catch (error) {
console.error('Error formatting time:', error, timestamp);
return '';
console.error('Error loading more messages:', error);
} finally {
isLoadingMore.value = false;
}
};
// Функция для отправки сообщения
const sendMessage = async () => {
if (!newMessage.value.trim() || isLoading.value) return;
console.log('Отправка сообщения:', newMessage.value, 'язык:', userLanguage.value);
// Если пользователь не аутентифицирован, используем sendGuestMessage
if (!auth.isAuthenticated) {
await sendGuestMessage();
return;
}
// Код для аутентифицированных пользователей
const userMessage = {
id: Date.now(),
content: newMessage.value,
role: 'user',
timestamp: new Date().toISOString()
};
messages.value.push(userMessage);
const messageText = newMessage.value;
newMessage.value = '';
// Прокрутка вниз
await nextTick();
// Функция прокрутки к последнему сообщению
const scrollToBottom = () => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
isLoading.value = true;
try {
const response = await axios.post('/api/chat/message', {
message: messageText,
language: userLanguage.value
});
console.log('Ответ от сервера:', response.data);
// Добавляем ответ от ИИ
messages.value.push({
id: Date.now() + 1,
content: response.data.message,
role: 'assistant',
timestamp: new Date().toISOString()
});
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
} catch (error) {
console.error('Ошибка при отправке сообщения:', error);
messages.value.push({
id: Date.now() + 1,
content: 'Произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте еще раз.',
role: 'assistant',
timestamp: new Date().toISOString()
});
} finally {
isLoading.value = false;
}
};
// Добавим наблюдатель за изменением состояния аутентификации
watch(() => auth.isAuthenticated, async (newValue, oldValue) => {
console.log('Auth state changed in HomeView:', newValue);
// Инициализация при монтировании
onMounted(async () => {
console.log('HomeView.vue: onMounted called');
console.log('Auth state:', auth.isAuthenticated);
if (newValue && !oldValue) {
// Пользователь только что аутентифицировался
// Определяем язык
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();
}
});
// Загрузка истории сообщений
const loadChatHistory = async () => {
console.log('Loading chat history...');
// Наблюдатель за изменением состояния аутентификации
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 {
console.log('User address from auth store:', auth.address);
// Добавляем заголовок авторизации
const headers = {};
if (auth.address) {
const authHeader = `Bearer ${auth.address}`;
console.log('Adding Authorization header:', authHeader);
headers.Authorization = authHeader;
}
const response = await axios.get('/api/chat/history', { headers });
console.log('Chat history response:', response.data);
if (response.data.messages) {
// Получаем историю с сервера
const serverMessages = response.data.messages.map(msg => ({
id: msg.id,
content: msg.content,
role: msg.role,
timestamp: msg.timestamp || msg.created_at,
isGuest: false
}));
// Объединяем гостевые сообщения с историей с сервера
// Сначала отправляем гостевые сообщения на сервер
await saveGuestMessagesToServer();
// Затем загружаем обновленную историю
const updatedResponse = await axios.get('/api/chat/history', { headers });
const updatedServerMessages = updatedResponse.data.messages.map(msg => ({
id: msg.id,
content: msg.content,
role: msg.role,
timestamp: msg.timestamp || msg.created_at,
isGuest: false
}));
// Обновляем сообщения
messages.value = updatedServerMessages;
// Очищаем гостевые сообщения
guestMessages.value = [];
localStorage.removeItem('guestMessages');
console.log('Updated messages:', messages.value);
const result = await connectWithWallet();
if (result.success) {
console.log('Wallet auth result:', result);
// Обновляем состояние аутентификации
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();
}
return result;
} catch (error) {
console.error('Error loading chat history:', error);
console.error('Error connecting wallet:', error);
throw error;
}
};
@@ -363,36 +373,29 @@ async function requestEmailCode() {
}
}
// Подтверждение кода подтверждения по email
async function verifyEmailCode() {
emailErrorMessage.value = '';
// Функция проверки кода
const verifyEmailCode = async () => {
try {
const response = await auth.verifyEmail(emailVerificationCode.value);
if (response.success) {
// Успешная верификация
const response = await axios.post('/api/auth/email/verify-code', {
email: emailInput.value,
code: emailCode.value
});
if (response.data.success) {
auth.setEmailAuth(response.data);
showEmailVerification.value = false;
emailVerificationCode.value = '';
emailError.value = '';
// Связываем гостевые сообщения с аутентифицированным пользователем
try {
await axios.post('/api/chat/link-guest-messages');
console.log('Guest messages linked to authenticated user');
} catch (linkError) {
console.error('Error linking guest messages:', linkError);
}
// Загружаем историю сообщений
// Загружаем историю чата после успешной аутентификации
await loadChatHistory();
} else {
emailErrorMessage.value = response.error || 'Неверный код подтверждения';
emailError.value = response.data.error || 'Неверный код';
}
} catch (error) {
emailError.value = error.response?.data?.error || 'Ошибка проверки кода';
console.error('Error verifying email code:', error);
emailErrorMessage.value = 'Ошибка верификации';
}
}
};
// Отмена верификации email
function cancelEmailVerification() {
@@ -407,117 +410,218 @@ const formatAddress = (address) => {
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
};
onMounted(async () => {
console.log('HomeView.vue: onMounted called');
console.log('Auth state:', auth.isAuthenticated);
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
// Определяем язык пользователя
const browserLanguage = navigator.language || navigator.userLanguage;
userLanguage.value = browserLanguage.split('-')[0];
console.log('Detected language:', userLanguage.value);
// Загружаем гостевые сообщения из localStorage
const savedGuestMessages = localStorage.getItem('guestMessages');
if (savedGuestMessages) {
guestMessages.value = JSON.parse(savedGuestMessages);
}
// Если пользователь аутентифицирован, загружаем историю чата с сервера
if (auth.isAuthenticated) {
console.log('User authenticated, loading chat history...');
await loadChatHistory();
} else {
// Если пользователь не аутентифицирован, отображаем гостевые сообщения
messages.value = [...guestMessages.value];
}
});
// Функция для отправки сообщения от неаутентифицированного пользователя
const sendGuestMessage = async () => {
if (!newMessage.value.trim()) return;
const userMessage = {
id: Date.now(),
content: newMessage.value,
role: 'user',
timestamp: new Date().toISOString(),
isGuest: true
};
// Добавляем сообщение пользователя в локальную историю
messages.value.push(userMessage);
// Сохраняем сообщение в массиве гостевых сообщений
guestMessages.value.push(userMessage);
// Сохраняем гостевые сообщения в localStorage
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Очищаем поле ввода
const messageText = newMessage.value;
newMessage.value = '';
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
// Устанавливаем состояние загрузки
isLoading.value = true;
// Отправляем запрос на сервер
try {
const response = await axios.post('/api/chat/guest-message', {
const date = new Date(timestamp);
// Проверяем, является ли дата валидной
if (isNaN(date.getTime())) {
console.warn('Invalid timestamp:', timestamp);
return '';
}
// Форматируем дату с указанием дня, месяца, года и времени
return date.toLocaleString([], {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
console.error('Error formatting time:', error, timestamp);
return '';
}
};
// Функция для отправки сообщения
const handleMessage = async (messageText) => {
if (!messageText.trim() || isLoading.value) return;
console.log('Handling message:', messageText);
isLoading.value = true;
try {
if (!auth.isAuthenticated) {
await sendGuestMessage(messageText);
} else {
await sendMessage(messageText);
}
} catch (error) {
console.error('Error handling 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
});
console.log('Response from server:', response.data);
// Добавляем ответ AI в историю
const aiMessage = {
id: Date.now() + 1,
content: response.data.message || response.data.reply,
role: 'assistant',
timestamp: new Date().toISOString(),
isGuest: true,
showAuthOptions: !hasShownAuthOptions.value
};
messages.value.push(aiMessage);
// Отмечаем, что опции аутентификации уже были показаны
if (!hasShownAuthOptions.value) {
hasShownAuthOptions.value = true;
}
// Сохраняем ответ AI в массиве гостевых сообщений
guestMessages.value.push(aiMessage);
// Обновляем localStorage
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
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 guest message:', 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: 'Произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте еще раз.',
content: 'Для получения ответа, пожалуйста, авторизуйтесь одним из способов:',
role: 'assistant',
timestamp: new Date().toISOString(),
isGuest: true
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;
}
};
// Добавляем методы для аутентификации
const handleTelegramAuth = () => {
window.open('https://t.me/HB3_Accelerator_Bot', '_blank');
// Показываем форму для ввода кода через небольшую задержку
setTimeout(() => {
showTelegramVerification.value = true;
}, 1000);
};
const handleEmailAuth = async () => {
showEmailForm.value = true;
};
// Функция отправки email
const submitEmail = async () => {
try {
const response = await axios.post('/api/auth/email/request', {
email: emailInput.value
});
if (response.data.success) {
showEmailForm.value = false;
showEmailVerification.value = true;
} else {
emailError.value = response.data.error || 'Ошибка отправки кода';
}
} catch (error) {
emailError.value = 'Ошибка отправки кода';
console.error('Error sending email code:', error);
}
};
// Функция верификации кода Telegram
const verifyTelegramCode = async () => {
try {
const response = await axios.post('/api/auth/telegram/verify', {
code: telegramCode.value
});
if (response.data.success) {
console.log('Telegram verification successful:', response.data);
// Обновляем состояние аутентификации
auth.setAuth({
isAuthenticated: response.data.authenticated,
userId: response.data.userId,
telegramId: response.data.telegramId,
isAdmin: response.data.isAdmin,
authType: 'telegram'
});
showTelegramVerification.value = false;
telegramCode.value = '';
// Показываем сообщение об успехе
messages.value.push({
id: Date.now(),
content: 'Telegram успешно подключен!',
role: 'assistant',
timestamp: new Date().toISOString()
});
// Загружаем историю чата после небольшой задержки
setTimeout(async () => {
await loadChatHistory();
}, 100);
} else {
messages.value.push({
id: Date.now(),
content: response.data.error || 'Ошибка верификации кода',
role: 'assistant',
timestamp: new Date().toISOString()
});
}
} catch (error) {
console.error('Error verifying Telegram code:', error);
messages.value.push({
id: Date.now(),
content: 'Произошла ошибка. Пожалуйста, попробуйте позже.',
role: 'assistant',
timestamp: new Date().toISOString()
});
}
};
</script>
<style scoped>
@@ -813,7 +917,7 @@ h1 {
.auth-btn {
display: flex;
align-items: center;
justify-content: flex-start;
justify-content: center;
padding: 0.75rem 1rem;
border-radius: 4px;
cursor: pointer;
@@ -858,4 +962,122 @@ h1 {
font-size: 0.9rem;
margin-top: 0.5rem;
}
.auth-buttons {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.auth-btn {
display: flex;
align-items: center;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.wallet-btn {
background-color: #4a5568;
color: white;
}
.telegram-btn {
background-color: #0088cc;
color: white;
}
.email-btn {
background-color: #48bb78;
color: white;
}
.auth-icon {
margin-right: 8px;
}
.email-form {
margin-top: 10px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.email-form input {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.email-form button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.email-form button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.auth-form {
margin-top: 10px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background: #f9f9f9;
}
.auth-input {
width: 100%;
padding: 8px 12px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.error-message {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
}
.load-more-container {
display: flex;
justify-content: center;
padding: 10px;
background-color: #f5f5f5;
position: sticky;
top: 0;
z-index: 1;
}
.load-more-btn {
padding: 8px 16px;
background-color: #4a5568;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.load-more-btn:hover:not(:disabled) {
background-color: #2d3748;
}
.load-more-btn:disabled {
background-color: #cbd5e0;
cursor: not-allowed;
}
</style>

View File

@@ -41,9 +41,7 @@ export default defineConfig({
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false,
ws: true,
changeOrigin: true
}
},
},