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

This commit is contained in:
2025-04-21 16:52:32 +03:00
parent ab05988017
commit 4648aab7d5
20 changed files with 4206 additions and 4068 deletions

View File

@@ -1,7 +1,8 @@
import globals from 'globals';
import * as vueParser from 'vue-eslint-parser';
import vuePlugin from 'eslint-plugin-vue';
import prettierPlugin from 'eslint-plugin-prettier';
import prettierConfig from '@vue/eslint-config-prettier';
import eslintConfigPrettier from 'eslint-config-prettier';
export default [
{
@@ -35,29 +36,37 @@ export default [
...globals.browser,
...globals.es2021,
},
parser: vuePlugin.parser,
parser: vueParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
sourceType: 'module',
ecmaVersion: 2022,
},
},
plugins: {
vue: vuePlugin,
prettier: prettierPlugin,
},
processor: vuePlugin.processors['.vue'],
rules: {
...prettierConfig.rules,
...vuePlugin.configs.base.rules,
...vuePlugin.configs['vue3-essential'].rules,
...vuePlugin.configs['vue3-strongly-recommended'].rules,
...vuePlugin.configs['vue3-recommended'].rules,
...eslintConfigPrettier.rules,
'prettier/prettier': 'warn',
'vue/comment-directive': 'off',
'vue/multi-word-component-names': 'off',
'vue/no-unused-vars': 'warn',
'vue/html-self-closing': ['warn', {
'vue/no-v-html': 'off',
'vue/html-self-closing': [
'warn',
{
html: {
void: 'always',
normal: 'always',
component: 'always'
}
}],
component: 'always',
},
},
],
'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
},
},

View File

@@ -1,137 +1,28 @@
<template>
<div id="app">
<router-view />
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner" />
</div>
<RouterView />
</div>
</template>
<script setup>
import { onMounted, ref, provide, computed } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { ref, watch } from 'vue';
import { RouterView } from 'vue-router';
import { useAuth } from './composables/useAuth';
import './assets/styles/home.css';
console.log('App.vue: Version with auth check loaded');
// Состояние загрузки
const isLoading = ref(false);
const router = useRouter();
// Использование composable для аутентификации
const { isAuthenticated } = useAuth();
// Создаем реактивное состояние с помощью ref
const authState = ref({
isAuthenticated: false,
userRole: null,
address: null
});
// Предоставляем состояние аутентификации всем компонентам
const auth = {
// Используем computed для реактивности
isAuthenticated: computed(() => authState.value.isAuthenticated),
userRole: computed(() => authState.value.userRole),
address: computed(() => authState.value.address),
async checkAuth() {
try {
const response = await axios.get('/api/auth/check');
console.log('Auth check response:', response.data);
authState.value = {
isAuthenticated: response.data.authenticated,
userRole: response.data.role,
address: response.data.address
};
console.log('Auth state updated:', authState.value);
} catch (error) {
console.error('Auth check failed:', error);
watch(isAuthenticated, (newValue, oldValue) => {
if (newValue !== oldValue) {
console.log('Состояние аутентификации изменилось:', newValue);
}
},
async disconnect() {
try {
await axios.post('/api/auth/logout');
authState.value = {
isAuthenticated: false,
userRole: null,
address: null
};
} catch (error) {
console.error('Logout failed:', error);
}
}
};
provide('auth', auth);
onMounted(async () => {
await auth.checkAuth();
});
</script>
<style>
body {
margin: 0;
font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333;
background-color: #f5f5f5;
}
#app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
flex: 1;
padding: 1rem;
}
button {
cursor: pointer;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 500;
border: none;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-secondary {
background-color: #95a5a6;
color: white;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@@ -15,8 +15,8 @@ const api = axios.create({
baseURL: getBaseUrl(),
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
'Content-Type': 'application/json',
},
});
// Перехватчик запросов
@@ -50,7 +50,7 @@ const sendGuestMessageToServer = async (messageText) => {
try {
await axios.post('/api/chat/guest-message', {
message: messageText,
language: userLanguage.value
// language: userLanguage.value, // TODO: Реализовать получение языка пользователя
});
} catch (error) {
console.error('Ошибка при отправке гостевого сообщения на сервер:', error);

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<div class="conversation-list">
<div class="list-header">
<h3>Диалоги</h3>
<button @click="createNewConversation" class="new-conversation-btn">
<button class="new-conversation-btn" @click="createNewConversation">
<span>+</span> Новый диалог
</button>
</div>
@@ -52,12 +52,15 @@ const loading = ref(true);
const selectedConversationId = ref(null);
// Следим за изменением статуса аутентификации
watch(() => isAuthenticated.value, (authenticated) => {
watch(
() => isAuthenticated.value,
(authenticated) => {
if (!authenticated) {
conversations.value = []; // Очищаем список бесед при отключении
selectedConversationId.value = null;
}
});
}
);
// Загрузка списка диалогов
const fetchConversations = async () => {
@@ -144,101 +147,3 @@ defineExpose({
fetchConversations,
});
</script>
<style scoped>
.conversation-list {
display: flex;
flex-direction: column;
width: 300px;
border-right: 1px solid #e0e0e0;
background-color: #f9f9f9;
height: 100%;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.list-header h3 {
margin: 0;
font-size: 1.2rem;
}
.new-conversation-btn {
display: flex;
align-items: center;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
padding: 0.5rem 0.75rem;
cursor: pointer;
font-size: 0.9rem;
}
.new-conversation-btn span {
font-size: 1.2rem;
margin-right: 0.25rem;
}
.loading,
.empty-list {
padding: 2rem;
text-align: center;
color: #666;
}
.empty-list p {
margin: 0.5rem 0;
}
.conversations {
flex: 1;
overflow-y: auto;
}
.conversation-item {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
cursor: pointer;
transition: background-color 0.2s;
}
.conversation-item:hover {
background-color: #f0f0f0;
}
.conversation-item.active {
background-color: #e8f5e9;
color: #4caf50;
}
.conversation-title {
font-weight: 500;
margin-bottom: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #888;
}
.time {
font-size: 0.8rem;
}
.connect-wallet-prompt {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@@ -1,14 +1,14 @@
<template>
<div class="message-input">
<textarea
ref="textareaRef"
v-model="message"
placeholder="Введите сообщение..."
@keydown.enter.prevent="handleEnter"
ref="textareaRef"
:disabled="sending"
></textarea>
@keydown.enter.prevent="handleEnter"
/>
<button @click="sendMessage" class="send-button" :disabled="!message.trim() || sending">
<button class="send-button" :disabled="!message.trim() || sending" @click="sendMessage">
<span v-if="sending">Отправка...</span>
<span v-else>Отправить</span>
</button>
@@ -51,7 +51,7 @@ const sendMessage = async () => {
id: Date.now(),
content: messageText,
role: auth.isAuthenticated ? 'user' : 'guest',
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
};
messages.value.push(userMessage);
@@ -60,12 +60,12 @@ const sendMessage = async () => {
// Логируем параметры запроса
console.log('Sending message to Ollama:', {
message: messageText,
language: userLanguage.value
language: userLanguage.value,
});
const response = await axios.post('/api/chat/message', {
message: messageText,
language: userLanguage.value
language: userLanguage.value,
});
// Логируем ответ от Ollama
@@ -76,7 +76,7 @@ const sendMessage = async () => {
id: Date.now() + 1,
content: response.data.message,
role: 'assistant',
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
// Очищаем поле ввода
@@ -115,7 +115,7 @@ const sendGuestMessage = async (messageText) => {
content: messageText,
role: 'user',
timestamp: new Date().toISOString(),
isGuest: true
isGuest: true,
};
// Добавляем сообщение пользователя в локальную историю
@@ -146,7 +146,7 @@ const sendGuestMessage = async (messageText) => {
role: 'assistant',
timestamp: new Date().toISOString(),
isGuest: true,
showAuthOptions: true // Указываем, что нужно показать кнопки аутентификации
showAuthOptions: true, // Указываем, что нужно показать кнопки аутентификации
};
messages.value.push(authMessage);
@@ -162,53 +162,3 @@ const sendGuestMessage = async (messageText) => {
isLoading.value = false;
};
</script>
<style scoped>
.message-input {
display: flex;
padding: 1rem;
border-top: 1px solid #e0e0e0;
background-color: #fff;
}
textarea {
flex: 1;
min-height: 40px;
max-height: 120px;
padding: 0.75rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
resize: none;
font-family: inherit;
font-size: 0.9rem;
line-height: 1.4;
}
textarea:focus {
outline: none;
border-color: #4caf50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.send-button {
margin-left: 0.5rem;
padding: 0 1rem;
height: 40px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
.send-button:hover:not(:disabled) {
background-color: #43a047;
}
.send-button:disabled {
background-color: #9e9e9e;
cursor: not-allowed;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div v-if="isAuthenticated">
<div class="message-thread" ref="threadContainer">
<div ref="threadContainer" class="message-thread">
<div v-if="loading" class="loading">Загрузка сообщений...</div>
<div v-else-if="messages.length === 0" class="empty-thread">
@@ -8,7 +8,11 @@
</div>
<div v-else class="messages">
<div v-for="message in messages" :key="message.id" :class="['message', message.sender_type]">
<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>
@@ -111,11 +115,14 @@ watch(
);
// Следим за изменением статуса аутентификации
watch(() => isAuthenticated.value, (authenticated) => {
watch(
() => isAuthenticated.value,
(authenticated) => {
if (!authenticated) {
messages.value = []; // Очищаем сообщения при отключении
}
});
}
);
// Загрузка сообщений при монтировании компонента
onMounted(() => {
@@ -130,84 +137,3 @@ defineExpose({
addMessages,
});
</script>
<style scoped>
.message-thread {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
}
.loading,
.empty-thread {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #666;
}
.messages {
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
max-width: 80%;
padding: 0.75rem 1rem;
border-radius: 8px;
position: relative;
}
.message.user {
align-self: flex-end;
background-color: #e3f2fd;
border: 1px solid #bbdefb;
}
.message.ai {
align-self: flex-start;
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
}
.message.admin {
align-self: flex-start;
background-color: #fff3e0;
border: 1px dashed #ffb74d;
}
.message-content {
white-space: pre-wrap;
word-break: break-word;
}
.message-meta {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.25rem;
font-size: 0.7rem;
color: #999;
}
.time {
font-size: 0.7rem;
}
.channel {
font-size: 0.7rem;
padding: 0.1rem 0.3rem;
border-radius: 3px;
background-color: #f0f0f0;
}
.connect-wallet-prompt {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@@ -1,41 +1,18 @@
<template>
<div class="email-connection">
<div v-if="!showVerification" class="email-form">
<input
v-model="email"
type="email"
placeholder="Введите email"
class="email-input"
/>
<button
@click="requestCode"
:disabled="isLoading || !isValidEmail"
class="email-btn"
>
<input v-model="email" type="email" placeholder="Введите email" class="email-input" />
<button :disabled="isLoading || !isValidEmail" class="email-btn" @click="requestCode">
{{ isLoading ? 'Отправка...' : 'Получить код' }}
</button>
</div>
<div v-else class="verification-form">
<p class="verification-info">Код отправлен на {{ email }}</p>
<input
v-model="code"
type="text"
placeholder="Введите код"
class="code-input"
/>
<button
@click="verifyCode"
:disabled="isLoading || !code"
class="verify-btn"
>
<input v-model="code" type="text" placeholder="Введите код" class="code-input" />
<button :disabled="isLoading || !code" class="verify-btn" @click="verifyCode">
{{ isLoading ? 'Проверка...' : 'Подтвердить' }}
</button>
<button
@click="resetForm"
class="reset-btn"
>
Изменить email
</button>
<button class="reset-btn" @click="resetForm">Изменить email</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
@@ -65,7 +42,7 @@ const requestCode = async () => {
error.value = '';
const response = await axios.post('/api/auth/email/request-verification', {
email: email.value
email: email.value,
});
if (response.data.success) {
@@ -87,7 +64,7 @@ const verifyCode = async () => {
const response = await axios.post('/api/auth/email/verify', {
email: email.value,
code: code.value
code: code.value,
});
if (response.data.success) {
@@ -111,63 +88,3 @@ const resetForm = () => {
showVerification.value = false;
};
</script>
<style scoped>
.email-connection {
padding: 20px;
max-width: 400px;
}
.email-form,
.verification-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.email-input,
.code-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.email-btn,
.verify-btn,
.reset-btn {
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.email-btn,
.verify-btn {
background-color: #48bb78;
color: white;
}
.reset-btn {
background-color: #e2e8f0;
color: #4a5568;
}
.verification-info {
color: #4a5568;
font-size: 14px;
}
.error {
color: #e53e3e;
margin-top: 5px;
font-size: 14px;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -2,7 +2,7 @@
<div class="telegram-connect">
<div v-if="!showQR" class="intro">
<p>Подключите свой аккаунт Telegram для быстрой авторизации</p>
<button @click="startConnection" class="connect-button" :disabled="loading">
<button class="connect-button" :disabled="loading" @click="startConnection">
<span class="telegram-icon">📱</span>
{{ loading ? 'Загрузка...' : 'Подключить Telegram' }}
</button>
@@ -10,14 +10,11 @@
<div v-else class="qr-section">
<p>Отсканируйте QR-код в приложении Telegram</p>
<div class="qr-container" v-html="qrCode"></div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="qr-container" v-html="qrCode" />
<p class="or-divider">или</p>
<a :href="botLink" target="_blank" class="bot-link">
Открыть бота в Telegram
</a>
<button @click="resetConnection" class="reset-button">
Отмена
</button>
<a :href="botLink" target="_blank" class="bot-link"> Открыть бота в Telegram </a>
<button class="reset-button" @click="resetConnection">Отмена</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
@@ -71,7 +68,7 @@ const startConnection = async () => {
const checkConnection = async () => {
try {
const response = await axios.post('/api/auth/telegram/check-connection', {
token: connectionToken.value
token: connectionToken.value,
});
if (response.data.success && response.data.telegramId) {
@@ -109,99 +106,3 @@ onUnmounted(() => {
stopPolling();
});
</script>
<style scoped>
.telegram-connect {
padding: 20px;
max-width: 400px;
}
.intro,
.qr-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
text-align: center;
}
.connect-button {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background-color: #0088cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
}
.connect-button:hover:not(:disabled) {
background-color: #0077b5;
}
.telegram-icon {
margin-right: 10px;
font-size: 18px;
}
.qr-container {
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.qr-container img {
max-width: 200px;
height: auto;
}
.or-divider {
color: #666;
margin: 10px 0;
}
.bot-link {
color: #0088cc;
text-decoration: none;
padding: 8px 16px;
border: 1px solid #0088cc;
border-radius: 4px;
transition: all 0.2s;
}
.bot-link:hover {
background-color: #0088cc;
color: white;
}
.reset-button {
padding: 8px 16px;
background-color: #e2e8f0;
color: #4a5568;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.reset-button:hover {
background-color: #cbd5e0;
}
.error {
color: #e53e3e;
margin-top: 10px;
text-align: center;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -2,11 +2,7 @@
<div class="wallet-connection">
<div v-if="!isConnected" class="connect-section">
<p>Подключите свой кошелек для доступа к расширенным функциям</p>
<button
@click="connectWallet"
:disabled="isLoading"
class="wallet-btn"
>
<button :disabled="isLoading" class="wallet-btn" @click="connectWallet">
<span class="wallet-icon">💳</span>
{{ isLoading ? 'Подключение...' : 'Подключить кошелек' }}
</button>
@@ -65,61 +61,3 @@ const connectWallet = async () => {
}
};
</script>
<style scoped>
.wallet-connection {
padding: 20px;
max-width: 400px;
}
.connect-section,
.status-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
text-align: center;
}
.wallet-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background-color: #4a5568;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
}
.wallet-btn:hover:not(:disabled) {
background-color: #2d3748;
}
.wallet-icon {
margin-right: 10px;
font-size: 18px;
}
.address {
font-family: monospace;
background-color: #f7fafc;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
}
.error {
color: #e53e3e;
margin-top: 10px;
text-align: center;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -2,8 +2,4 @@ import TelegramConnect from './TelegramConnect.vue';
import WalletConnection from './WalletConnection.vue';
import EmailConnect from './EmailConnect.vue';
export {
TelegramConnect,
WalletConnection,
EmailConnect
};
export { TelegramConnect, WalletConnection, EmailConnect };

View File

@@ -22,10 +22,10 @@ export function useAuth() {
if (response.data.success) {
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
const filteredIdentities = response.data.identities
.filter(identity => identity.provider !== 'guest')
.filter((identity) => identity.provider !== 'guest')
.reduce((acc, identity) => {
// Для каждого типа провайдера оставляем только один идентификатор
const existingIdentity = acc.find(i => i.provider === identity.provider);
const existingIdentity = acc.find((i) => i.provider === identity.provider);
if (!existingIdentity) {
acc.push(identity);
}
@@ -69,7 +69,15 @@ export function useAuth() {
}
};
const updateAuth = async ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, isAdmin: newIsAdmin, email: newEmail }) => {
const updateAuth = async ({
authenticated,
authType: newAuthType,
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail,
}) => {
const wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value;
@@ -80,7 +88,7 @@ export function useAuth() {
newAddress,
newTelegramId,
newIsAdmin,
newEmail
newEmail,
});
// Убедимся, что переменные являются реактивными
@@ -93,15 +101,18 @@ export function useAuth() {
email.value = newEmail || null;
// Кэшируем данные аутентификации
localStorage.setItem('authData', JSON.stringify({
localStorage.setItem(
'authData',
JSON.stringify({
authenticated,
authType: newAuthType,
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail
}));
email: newEmail,
})
);
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
@@ -123,7 +134,7 @@ export function useAuth() {
address: address.value,
telegramId: telegramId.value,
email: email.value,
isAdmin: isAdmin.value
isAdmin: isAdmin.value,
});
// Если пользователь только что аутентифицировался или сменил аккаунт,
@@ -149,14 +160,14 @@ export function useAuth() {
return {
success: true,
message: 'No new guest IDs to process',
processedIds: processedGuestIds.value
processedIds: processedGuestIds.value,
};
}
// Создаем объект с идентификаторами для передачи на сервер
const identifiersData = {
userId: userId.value,
guestId: localGuestId
guestId: localGuestId,
};
// Добавляем все доступные идентификаторы
@@ -181,11 +192,13 @@ export function useAuth() {
// В качестве запасного варианта также обрабатываем старый формат ответа
else if (response.data.results && Array.isArray(response.data.results)) {
const newProcessedIds = response.data.results
.filter(result => result.guestId)
.map(result => result.guestId);
.filter((result) => result.guestId)
.map((result) => result.guestId);
if (newProcessedIds.length > 0) {
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...newProcessedIds])];
processedGuestIds.value = [
...new Set([...processedGuestIds.value, ...newProcessedIds]),
];
console.log('Updated processed guest IDs from results:', processedGuestIds.value);
}
}
@@ -196,14 +209,14 @@ export function useAuth() {
return {
success: true,
processedIds: processedGuestIds.value
processedIds: processedGuestIds.value,
};
}
} catch (error) {
console.error('Error linking messages:', error);
return {
success: false,
error: error.message
error: error.message,
};
}
}
@@ -232,7 +245,7 @@ export function useAuth() {
address: response.data.address,
telegramId: response.data.telegramId,
email: response.data.email,
isAdmin: response.data.isAdmin
isAdmin: response.data.isAdmin,
});
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
@@ -285,7 +298,7 @@ export function useAuth() {
address: null,
telegramId: null,
email: null,
isAdmin: false
isAdmin: false,
});
// Обновляем отображение отключенного состояния
@@ -400,7 +413,7 @@ export function useAuth() {
const response = await axios.post('/api/auth/identities/link', {
type: provider,
value: providerId
value: providerId,
});
if (response.data.success) {
@@ -426,7 +439,7 @@ export function useAuth() {
console.error('Ошибка при связывании идентификатора:', error);
return {
success: false,
error: error.response?.data?.error || error.message
error: error.response?.data?.error || error.message,
};
}
};
@@ -449,6 +462,6 @@ export function useAuth() {
updateIdentities,
updateProcessedGuestIds,
updateConnectionDisplay,
linkIdentity
linkIdentity,
};
}

View File

@@ -8,7 +8,8 @@ import axios from 'axios';
// Настройка axios
// В Docker контейнере localhost:8000 не работает, поэтому используем явное значение
const apiUrl = window.location.hostname === 'localhost' ? 'http://localhost:8000' : import.meta.env.VITE_API_URL;
const apiUrl =
window.location.hostname === 'localhost' ? 'http://localhost:8000' : import.meta.env.VITE_API_URL;
axios.defaults.baseURL = apiUrl;
axios.defaults.withCredentials = true;

View File

@@ -8,13 +8,13 @@ const routes = [
{
path: '/',
name: 'home',
component: HomeView
}
component: HomeView,
},
];
const router = createRouter({
history: createWebHistory(),
routes
routes,
});
console.log('router/index.js: Router created');
@@ -27,7 +27,7 @@ router.beforeEach(async (to, from, next) => {
}
// Проверяем аутентификацию, если маршрут требует авторизации
if (to.matched.some(record => record.meta.requiresAuth)) {
if (to.matched.some((record) => record.meta.requiresAuth)) {
try {
const response = await axios.get('/api/auth/check');
if (response.data.authenticated) {

View File

@@ -3,25 +3,25 @@ import api from '../api/axios';
// Адреса смарт-контрактов токенов HB3A
export const TOKEN_CONTRACTS = {
eth: {
address: "0xd95a45fc46a7300e6022885afec3d618d7d3f27c",
symbol: "HB3A",
network: "Ethereum"
address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c',
symbol: 'HB3A',
network: 'Ethereum',
},
bsc: {
address: "0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D",
symbol: "HB3A",
network: "BSC"
address: '0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D',
symbol: 'HB3A',
network: 'BSC',
},
arbitrum: {
address: "0xdce769b847a0a697239777d0b1c7dd33b6012ba0",
symbol: "HB3A",
network: "Arbitrum"
address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0',
symbol: 'HB3A',
network: 'Arbitrum',
},
polygon: {
address: "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d",
symbol: "HB3A",
network: "Polygon"
}
address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d',
symbol: 'HB3A',
network: 'Polygon',
},
};
// Получение балансов токенов
@@ -35,7 +35,7 @@ export const fetchTokenBalances = async () => {
eth: '0',
bsc: '0',
arbitrum: '0',
polygon: '0'
polygon: '0',
};
}
};

View File

@@ -45,7 +45,7 @@ export async function connectWithWallet() {
version: '1',
chainId: 1,
nonce,
resources: [`${origin}/api/auth/verify`]
resources: [`${origin}/api/auth/verify`],
});
const message = siweMessage.prepareMessage();
@@ -55,7 +55,7 @@ export async function connectWithWallet() {
console.log('Requesting signature...');
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, address]
params: [message, address],
});
console.log('Got signature:', signature);
@@ -65,7 +65,7 @@ export async function connectWithWallet() {
const verificationResponse = await axios.post('/api/auth/verify', {
message,
signature,
address
address,
});
console.log('Verification response:', verificationResponse.data);

View File

@@ -11,7 +11,8 @@ export const connectWallet = async () => {
console.error('No Ethereum provider (like MetaMask) detected!');
return {
success: false,
error: 'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.'
error:
'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.',
};
}
@@ -24,7 +25,7 @@ export const connectWallet = async () => {
if (!accounts || accounts.length === 0) {
return {
success: false,
error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.'
error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.',
};
}
@@ -43,7 +44,7 @@ export const connectWallet = async () => {
if (!nonce) {
return {
success: false,
error: 'Не удалось получить nonce от сервера.'
error: 'Не удалось получить nonce от сервера.',
};
}
@@ -65,7 +66,7 @@ export const connectWallet = async () => {
chainId: 1, // Ethereum mainnet
nonce: nonce,
issuedAt: new Date().toISOString(),
resources: [`${origin}/api/auth/verify`]
resources: [`${origin}/api/auth/verify`],
});
// Получаем строку сообщения для подписи
@@ -79,7 +80,7 @@ export const connectWallet = async () => {
if (!signature) {
return {
success: false,
error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.'
error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.',
};
}
@@ -90,7 +91,7 @@ export const connectWallet = async () => {
const verifyResponse = await axios.post('/api/auth/verify', {
address: normalizedAddress,
signature,
nonce
nonce,
});
// Обновляем интерфейс для отображения подключенного состояния
@@ -118,12 +119,12 @@ export const connectWallet = async () => {
success: true,
address: normalizedAddress,
userId: verifyResponse.data.userId,
isAdmin: verifyResponse.data.isAdmin
isAdmin: verifyResponse.data.isAdmin,
};
} else {
return {
success: false,
error: verifyResponse.data.error || 'Ошибка верификации на сервере.'
error: verifyResponse.data.error || 'Ошибка верификации на сервере.',
};
}
} catch (error) {
@@ -142,7 +143,7 @@ export const connectWallet = async () => {
return {
success: false,
error: errorMessage
error: errorMessage,
};
}
};

View File

@@ -8,25 +8,41 @@
<h1 class="title">HB3 - Accelerator DLE</h1>
<p class="subtitle">Венчурный фонд и поставщик программного обеспечения</p>
</div>
<button class="nav-btn header-wallet-btn" @click="toggleWalletSidebar" :class="{ active: showWalletSidebar }">
<div class="hamburger-line"></div>
<div class="nav-btn-number">7</div>
<div class="nav-btn-text">{{ showWalletSidebar ? 'Скрыть панель' : 'Подключиться' }}</div>
<button
class="header-wallet-btn"
:class="{ active: showWalletSidebar }"
@click="toggleWalletSidebar"
>
<div class="hamburger-line" />
<div class="nav-btn-text">
{{ showWalletSidebar ? 'Скрыть панель' : 'Подключиться' }}
</div>
</button>
</div>
</div>
<div class="chat-container">
<div class="chat-messages" ref="messagesContainer">
<div v-for="message in messages" :key="message.id"
:class="['message',
message.sender_type === 'assistant' || message.role === 'assistant' ? 'ai-message' :
message.sender_type === 'system' || message.role === 'system' ? 'system-message' : 'user-message',
<div ref="messagesContainer" class="chat-messages">
<div
v-for="message in messages"
:key="message.id"
:class="[
'message',
message.sender_type === 'assistant' || message.role === 'assistant'
? 'ai-message'
: message.sender_type === 'system' || message.role === 'system'
? 'system-message'
: 'user-message',
message.isLocal ? 'is-local' : '',
message.hasError ? 'has-error' : '']">
<div class="message-content" v-html="formatMessage(message.content)"></div>
message.hasError ? 'has-error' : '',
]"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="message-content" v-html="formatMessage(message.content)" />
<div class="message-meta">
<div class="message-time">{{ formatTime(message.timestamp || message.created_at) }}</div>
<div class="message-time">
{{ formatTime(message.timestamp || message.created_at) }}
</div>
<div v-if="message.isLocal" class="message-status">
<span class="sending-indicator">Отправка...</span>
</div>
@@ -39,21 +55,21 @@
<div class="chat-input">
<textarea
ref="messageInput"
v-model="newMessage"
placeholder="Введите сообщение..."
:disabled="isLoading"
rows="3"
autofocus
@keydown.enter.prevent="handleMessage(newMessage)"
@focus="handleFocus"
@blur="handleBlur"
:disabled="isLoading"
ref="messageInput"
rows="3"
autofocus
></textarea>
/>
<div class="chat-buttons">
<button @click="handleMessage(newMessage)" :disabled="isLoading || !newMessage.trim()">
<button :disabled="isLoading || !newMessage.trim()" @click="handleMessage(newMessage)">
{{ isLoading ? 'Отправка...' : 'Отправить' }}
</button>
<button @click="clearGuestMessages" class="clear-btn" :disabled="isLoading">
<button class="clear-btn" :disabled="isLoading" @click="clearGuestMessages">
Очистить
</button>
</div>
@@ -62,33 +78,67 @@
</div>
<!-- Правая панель с информацией о кошельке -->
<div class="wallet-sidebar" v-if="showWalletSidebar">
<transition name="sidebar-slide">
<div v-if="showWalletSidebar" class="wallet-sidebar">
<div class="wallet-sidebar-content">
<!-- Блок для неавторизованных пользователей -->
<div v-if="!isAuthenticated" class="auth-container">
<div class="wallet-header">
<button v-if="!isAuthenticated" @click="handleWalletAuth" class="wallet-connect-btn-header">
Подключить кошелек
</button>
<button v-if="isAuthenticated" @click="disconnectWallet" class="wallet-disconnect-btn-header">
Отключить
</button>
<div class="wallet-header-buttons">
<button class="close-wallet-sidebar" @click="toggleWalletSidebar">×</button>
</div>
</div>
<!-- Добавляем дополнительные кнопки авторизации -->
<div v-if="!isAuthenticated" class="auth-buttons">
<h3>Авторизация через:</h3>
<div v-if="!telegramAuth.showVerification" class="auth-btn-container">
<button @click="handleTelegramAuth" class="auth-btn telegram-btn">
<div class="auth-buttons-wrapper">
<button
v-if="
!telegramAuth.showVerification &&
!emailAuth.showForm &&
!emailAuth.showVerification
"
class="auth-btn connect-wallet-btn"
@click="handleWalletAuth"
>
Подключить кошелек
</button>
<button
v-if="
!telegramAuth.showVerification &&
!emailAuth.showForm &&
!emailAuth.showVerification
"
class="auth-btn telegram-btn"
@click="handleTelegramAuth"
>
Подключить Telegram
</button>
<button
v-if="
!telegramAuth.showVerification &&
!emailAuth.showForm &&
!emailAuth.showVerification
"
class="auth-btn email-btn"
@click="handleEmailAuth"
>
Подключить Email
</button>
</div>
<div v-if="telegramAuth.showVerification" class="verification-block">
<div class="verification-code">
<span>Код верификации:</span>
<code @click="copyCode(telegramAuth.verificationCode)">{{ telegramAuth.verificationCode }}</code>
<code @click="copyCode(telegramAuth.verificationCode)">{{
telegramAuth.verificationCode
}}</code>
<span v-if="codeCopied" class="copied-message">Скопировано!</span>
</div>
<a :href="telegramAuth.botLink" target="_blank" class="bot-link">Открыть бота Telegram</a>
<button @click="cancelTelegramAuth" class="cancel-btn">Отмена</button>
<a :href="telegramAuth.botLink" target="_blank" class="bot-link"
>Открыть бота Telegram</a
>
<button class="cancel-btn" @click="cancelTelegramAuth">Отмена</button>
</div>
<!-- Сообщение об ошибке в Telegram -->
@@ -97,12 +147,6 @@
<button class="close-error" @click="telegramAuth.error = ''">×</button>
</div>
<div v-if="!emailAuth.showForm && !emailAuth.showVerification" class="auth-btn-container">
<button @click="handleEmailAuth" class="auth-btn email-btn">
Подключить Email
</button>
</div>
<!-- Форма для Email верификации -->
<div v-if="emailAuth.showForm" class="email-form">
<p>Введите ваш email для получения кода подтверждения:</p>
@@ -114,19 +158,28 @@
class="email-input"
:class="{ 'email-input-error': emailAuth.formatError }"
/>
<button @click="sendEmailVerification" class="send-email-btn" :disabled="emailAuth.isLoading">
<button
class="send-email-btn"
:disabled="emailAuth.isLoading"
@click="sendEmailVerification"
>
{{ emailAuth.isLoading ? 'Отправка...' : 'Отправить код' }}
</button>
</div>
<div class="form-actions">
<button @click="cancelEmailAuth" class="cancel-btn">Отмена</button>
<p v-if="emailAuth.formatError" class="email-format-error">Пожалуйста, введите корректный email</p>
<button class="cancel-btn" @click="cancelEmailAuth">Отмена</button>
<p v-if="emailAuth.formatError" class="email-format-error">
Пожалуйста, введите корректный email
</p>
</div>
</div>
<!-- Форма для ввода кода верификации Email -->
<div v-if="emailAuth.showVerification" class="email-verification-form">
<p>На ваш email <strong>{{ emailAuth.verificationEmail }}</strong> отправлен код подтверждения.</p>
<p>
На ваш email <strong>{{ emailAuth.verificationEmail }}</strong> отправлен код
подтверждения.
</p>
<div class="email-form-container">
<input
v-model="emailAuth.verificationCode"
@@ -135,12 +188,15 @@
maxlength="6"
class="email-input"
/>
<button @click="verifyEmailCode" class="send-email-btn" :disabled="emailAuth.isVerifying">
<button
class="send-email-btn"
:disabled="emailAuth.isVerifying"
@click="verifyEmailCode"
>
{{ emailAuth.isVerifying ? 'Проверка...' : 'Подтвердить' }}
</button>
</div>
<button @click="cancelEmailAuth" class="cancel-btn">Отмена</button>
</div>
<button class="cancel-btn" @click="cancelEmailAuth">Отмена</button>
</div>
<!-- Сообщение об ошибке в Email -->
@@ -148,16 +204,28 @@
{{ emailAuth.error }}
<button class="close-error" @click="emailAuth.error = ''">×</button>
</div>
</div>
<!-- Блок для авторизованных пользователей -->
<div v-if="isAuthenticated" class="auth-buttons-container">
<div class="wallet-header">
<div class="wallet-header-buttons">
<button class="auth-btn disconnect-wallet-btn" @click="disconnectWallet">
Отключить
</button>
<button class="close-wallet-sidebar" @click="toggleWalletSidebar">×</button>
</div>
</div>
<!-- Блок информации о пользователе -->
<div v-if="isAuthenticated" class="user-info">
<div class="user-info">
<h3>Идентификаторы:</h3>
<div class="user-info-item">
<span class="user-info-label">Кошелек:</span>
<span v-if="hasIdentityType('wallet')" class="user-info-value">
{{ truncateAddress(getIdentityValue('wallet')) }}
</span>
<button v-else @click="handleWalletAuth" class="connect-btn">
<button v-else class="connect-btn" @click="handleWalletAuth">
Подключить кошелек
</button>
</div>
@@ -166,7 +234,7 @@
<span v-if="hasIdentityType('telegram')" class="user-info-value">
{{ getIdentityValue('telegram') }}
</span>
<button v-else @click="handleTelegramAuth" class="connect-btn">
<button v-else class="connect-btn" @click="handleTelegramAuth">
Подключить Telegram
</button>
</div>
@@ -175,14 +243,21 @@
<span v-if="hasIdentityType('email')" class="user-info-value">
{{ getIdentityValue('email') }}
</span>
<button v-else @click="handleEmailAuth" class="connect-btn">
<button v-else class="connect-btn" @click="handleEmailAuth">
Подключить Email
</button>
</div>
</div>
</div>
<!-- Блок форм подключения для аутентифицированных пользователей -->
<div v-if="isAuthenticated && (emailAuth.showForm || telegramAuth.showVerification || emailAuth.showVerification)" class="connect-forms">
<div
v-if="
isAuthenticated &&
(emailAuth.showForm || telegramAuth.showVerification || emailAuth.showVerification)
"
class="connect-forms"
>
<!-- Форма для Email верификации -->
<div v-if="emailAuth.showForm" class="email-form">
<p>Введите ваш email для получения кода подтверждения:</p>
@@ -194,19 +269,28 @@
class="email-input"
:class="{ 'email-input-error': emailAuth.formatError }"
/>
<button @click="sendEmailVerification" class="send-email-btn" :disabled="emailAuth.isLoading">
<button
class="send-email-btn"
:disabled="emailAuth.isLoading"
@click="sendEmailVerification"
>
{{ emailAuth.isLoading ? 'Отправка...' : 'Отправить код' }}
</button>
</div>
<div class="form-actions">
<button @click="cancelEmailAuth" class="cancel-btn">Отмена</button>
<p v-if="emailAuth.formatError" class="email-format-error">Пожалуйста, введите корректный email</p>
<button class="cancel-btn" @click="cancelEmailAuth">Отмена</button>
<p v-if="emailAuth.formatError" class="email-format-error">
Пожалуйста, введите корректный email
</p>
</div>
</div>
<!-- Форма для ввода кода верификации Email -->
<div v-if="emailAuth.showVerification" class="email-verification-form">
<p>На ваш email <strong>{{ emailAuth.verificationEmail }}</strong> отправлен код подтверждения.</p>
<p>
На ваш email <strong>{{ emailAuth.verificationEmail }}</strong> отправлен код
подтверждения.
</p>
<div class="email-form-container">
<input
v-model="emailAuth.verificationCode"
@@ -215,22 +299,30 @@
maxlength="6"
class="email-input"
/>
<button @click="verifyEmailCode" class="send-email-btn" :disabled="emailAuth.isVerifying">
<button
class="send-email-btn"
:disabled="emailAuth.isVerifying"
@click="verifyEmailCode"
>
{{ emailAuth.isVerifying ? 'Проверка...' : 'Подтвердить' }}
</button>
</div>
<button @click="cancelEmailAuth" class="cancel-btn">Отмена</button>
<button class="cancel-btn" @click="cancelEmailAuth">Отмена</button>
</div>
<!-- Форма для Telegram верификации -->
<div v-if="telegramAuth.showVerification" class="verification-block">
<div class="verification-code">
<span>Код верификации:</span>
<code @click="copyCode(telegramAuth.verificationCode)">{{ telegramAuth.verificationCode }}</code>
<code @click="copyCode(telegramAuth.verificationCode)">{{
telegramAuth.verificationCode
}}</code>
<span v-if="codeCopied" class="copied-message">Скопировано!</span>
</div>
<a :href="telegramAuth.botLink" target="_blank" class="bot-link">Открыть бота Telegram</a>
<button @click="cancelTelegramAuth" class="cancel-btn">Отмена</button>
<a :href="telegramAuth.botLink" target="_blank" class="bot-link"
>Открыть бота Telegram</a
>
<button class="cancel-btn" @click="cancelTelegramAuth">Отмена</button>
</div>
</div>
@@ -249,7 +341,9 @@
</div>
<div class="token-balance">
<span class="token-name">ARB:</span>
<span class="token-amount">{{ Number(tokenBalances.arbitrum).toLocaleString() }}</span>
<span class="token-amount">{{
Number(tokenBalances.arbitrum).toLocaleString()
}}</span>
<span class="token-symbol">{{ TOKEN_CONTRACTS.arbitrum.symbol }}</span>
</div>
<div class="token-balance">
@@ -260,6 +354,8 @@
</div>
</div>
</div>
</transition>
</div>
</template>
<script setup>
@@ -384,7 +480,7 @@ const formatTime = (timestamp) => {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
minute: '2-digit',
});
} catch (error) {
console.error('Error formatting time:', error, timestamp);
@@ -424,7 +520,7 @@ const telegramAuth = ref({
verificationCode: '',
botLink: '',
checkInterval: null,
error: ''
error: '',
});
// Объединяем состояния для Email аутентификации
@@ -437,13 +533,13 @@ const emailAuth = ref({
formatError: false,
isLoading: false,
isVerifying: false,
error: ''
error: '',
});
// Состояния для уведомлений
const notifications = ref({
successMessage: '',
showSuccess: false
showSuccess: false,
});
// Состояния для пагинации и загрузки сообщений
@@ -453,7 +549,7 @@ const messageLoading = ref({
offset: 0,
limit: 30,
isInProgress: false,
isLinkingGuest: false
isLinkingGuest: false,
});
// Состояние для балансов токенов
@@ -461,7 +557,7 @@ const tokenBalances = ref({
eth: '0',
bsc: '0',
arbitrum: '0',
polygon: '0'
polygon: '0',
});
// Переменная для хранения интервала обновления балансов
@@ -489,7 +585,7 @@ const guestIdValue = computed(() => {
const filteredIdentities = computed(() => {
if (!auth.identities || !auth.identities.value) return [];
return auth.identities.value.filter(identity => {
return auth.identities.value.filter((identity) => {
if (identity.provider === 'wallet' && auth.address?.value === identity.provider_id) {
return false;
}
@@ -507,8 +603,9 @@ const filteredIdentities = computed(() => {
* Определяет, нужно ли загружать историю сообщений
*/
const shouldLoadHistory = computed(() => {
return isAuthenticated.value ||
(getFromStorage('guestId') && getFromStorage('guestId').length > 0);
return (
isAuthenticated.value || (getFromStorage('guestId') && getFromStorage('guestId').length > 0)
);
});
/**
@@ -518,10 +615,10 @@ const shouldLoadHistory = computed(() => {
*/
const formatIdentityProvider = (provider) => {
const providers = {
'wallet': 'Кошелек',
'email': 'Email',
'telegram': 'Telegram',
'guest': 'Гость'
wallet: 'Кошелек',
email: 'Email',
telegram: 'Telegram',
guest: 'Гость',
};
return providers[provider] || provider;
};
@@ -544,18 +641,22 @@ const loadMessages = async (options = {}) => {
messageLoading.value.isInProgress = true;
if (!silent) isLoading.value = true;
console.log(`Загрузка истории сообщений${authType ? ` после ${authType} аутентификации` : ''}...`);
console.log(
`Загрузка истории сообщений${authType ? ` после ${authType} аутентификации` : ''}...`
);
// Если это загрузка после аутентификации, немного ждем для завершения привязки сообщений
if (authType) {
console.log(`Ожидание завершения привязки гостевых сообщений после ${authType} аутентификации...`);
console.log(
`Ожидание завершения привязки гостевых сообщений после ${authType} аутентификации...`
);
// Задержка для гарантии, что сервер успеет обработать сессию
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Дополнительно проверяем, что процесс связывания сообщений завершился
if (messageLoading.value.isLinkingGuest) {
await new Promise(resolve => {
await new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (!messageLoading.value.isLinkingGuest) {
clearInterval(checkInterval);
@@ -596,7 +697,7 @@ const loadMessages = async (options = {}) => {
// Запрашиваем общее количество сообщений
const countResponse = await axios.get('/api/chat/history', {
params: { count_only: true }
params: { count_only: true },
});
if (!countResponse.data.success) {
@@ -616,8 +717,8 @@ const loadMessages = async (options = {}) => {
const response = await axios.get('/api/chat/history', {
params: {
offset: effectiveOffset,
limit: messageLoading.value.limit
}
limit: messageLoading.value.limit,
},
});
if (response.data.success) {
@@ -651,9 +752,11 @@ const loadMessages = async (options = {}) => {
// Уведомляем о загрузке сообщений
if (authType) {
window.dispatchEvent(new CustomEvent('messages-updated', {
detail: { count: messages.value.length }
}));
window.dispatchEvent(
new CustomEvent('messages-updated', {
detail: { count: messages.value.length },
})
);
}
// Прокручиваем к последнему сообщению
@@ -688,7 +791,7 @@ const handleMessage = async (text) => {
role: 'user',
isLocal: true,
isGuest: !auth.isAuthenticated.value,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
};
// Добавляем сообщение в чат
@@ -708,12 +811,12 @@ const handleMessage = async (text) => {
// Отправляем сообщение как авторизованный пользователь
const response = await axios.post('/api/chat/message', {
message: userMessageContent,
language: userLanguage.value
language: userLanguage.value,
});
if (response.data.success) {
// Обновляем ID сообщения пользователя
const userMsgIndex = messages.value.findIndex(m => m.id === tempId);
const userMsgIndex = messages.value.findIndex((m) => m.id === tempId);
if (userMsgIndex !== -1) {
messages.value[userMsgIndex].id = response.data.userMessage.id;
messages.value[userMsgIndex].isLocal = false;
@@ -725,7 +828,7 @@ const handleMessage = async (text) => {
content: response.data.aiMessage.content,
sender_type: 'assistant',
role: 'assistant',
timestamp: response.data.aiMessage.created_at
timestamp: response.data.aiMessage.created_at,
});
// Прокручиваем к последнему сообщению
@@ -745,14 +848,14 @@ const handleMessage = async (text) => {
const response = await axios.post('/api/chat/guest-message', {
content: userMessageContent,
guestId,
language: userLanguage.value
language: userLanguage.value,
});
if (response.data.success) {
console.log('Гостевое сообщение отправлено:', response.data);
// Обновляем ID сообщения пользователя
const userMsgIndex = messages.value.findIndex(m => m.id === tempId);
const userMsgIndex = messages.value.findIndex((m) => m.id === tempId);
if (userMsgIndex !== -1) {
messages.value[userMsgIndex].id = response.data.messageId;
messages.value[userMsgIndex].isLocal = false;
@@ -767,7 +870,7 @@ const handleMessage = async (text) => {
sender_type: 'user',
role: 'user',
isGuest: true,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
setToStorage('guestMessages', JSON.stringify(storedMessages));
setToStorage('hasUserSentMessage', 'true');
@@ -787,7 +890,7 @@ const handleMessage = async (text) => {
console.error('Ошибка отправки сообщения:', error);
// Помечаем сообщение как ошибочное
const userMsgIndex = messages.value.findIndex(m => m.id === tempId);
const userMsgIndex = messages.value.findIndex((m) => m.id === tempId);
if (userMsgIndex !== -1) {
messages.value[userMsgIndex].hasError = true;
}
@@ -798,7 +901,7 @@ const handleMessage = async (text) => {
content: 'Произошла ошибка при отправке сообщения. Пожалуйста, попробуйте еще раз.',
sender_type: 'system',
role: 'system',
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
scrollToBottom();
} finally {
@@ -839,7 +942,7 @@ const scrollToBottom = () => {
const clearGuestMessages = () => {
removeFromStorage('guestMessages');
console.log('Гостевые сообщения очищены');
messages.value = messages.value.filter(m => !m.isGuest);
messages.value = messages.value.filter((m) => !m.isGuest);
};
/**
@@ -1013,14 +1116,16 @@ const handleTelegramAuth = async () => {
const linkResult = await auth.linkIdentity('telegram', telegramId);
if (linkResult.success) {
notifications.value.successMessage = 'Telegram успешно подключен к вашему аккаунту!';
notifications.value.successMessage =
'Telegram успешно подключен к вашему аккаунту!';
notifications.value.showSuccess = true;
setTimeout(() => {
notifications.value.showSuccess = false;
}, 3000);
} else {
notifications.value.errorMessage = linkResult.error || 'Не удалось подключить Telegram';
notifications.value.errorMessage =
linkResult.error || 'Не удалось подключить Telegram';
notifications.value.showError = true;
setTimeout(() => {
@@ -1045,7 +1150,8 @@ const handleTelegramAuth = async () => {
}
}, 2000); // Проверяем каждые 2 секунды
} else {
telegramAuth.value.error = response.data.error || 'Ошибка при инициализации авторизации Telegram';
telegramAuth.value.error =
response.data.error || 'Ошибка при инициализации авторизации Telegram';
telegramAuth.value.showVerification = false;
}
} catch (error) {
@@ -1128,9 +1234,13 @@ const sendEmailVerification = async () => {
emailAuth.value.verificationEmail = emailAuth.value.email;
emailAuth.value.verificationCode = '';
console.log('Отображаем форму ввода кода верификации для email:', emailAuth.value.verificationEmail);
console.log(
'Отображаем форму ввода кода верификации для email:',
emailAuth.value.verificationEmail
);
} else {
emailAuth.value.error = response.data.error || 'Ошибка инициализации аутентификации по email';
emailAuth.value.error =
response.data.error || 'Ошибка инициализации аутентификации по email';
emailAuth.value.showForm = true;
emailAuth.value.showVerification = false;
}
@@ -1164,7 +1274,7 @@ const verifyEmailCode = async () => {
const response = await axios.post('/api/auth/email/verify-code', {
email: emailAuth.value.verificationEmail,
code: emailAuth.value.verificationCode
code: emailAuth.value.verificationCode,
});
if (response.data.success) {
@@ -1178,7 +1288,10 @@ const verifyEmailCode = async () => {
if (auth.isAuthenticated.value && emailAuth.value.verificationEmail) {
// Если пользователь уже авторизован, связываем email
if (auth.authType.value !== 'email') {
console.log('Связывание Email с существующим аккаунтом:', emailAuth.value.verificationEmail);
console.log(
'Связывание Email с существующим аккаунтом:',
emailAuth.value.verificationEmail
);
const linkResult = await auth.linkIdentity('email', emailAuth.value.verificationEmail);
if (linkResult.success) {
@@ -1297,7 +1410,7 @@ const disconnectWallet = async () => {
*/
const hasIdentityType = (type) => {
if (!auth.identities?.value) return false;
return auth.identities.value.some(identity => identity.provider === type);
return auth.identities.value.some((identity) => identity.provider === type);
};
/**
@@ -1307,7 +1420,7 @@ const hasIdentityType = (type) => {
*/
const getIdentityValue = (type) => {
if (!auth.identities?.value) return null;
const identity = auth.identities.value.find(identity => identity.provider === type);
const identity = auth.identities.value.find((identity) => identity.provider === type);
return identity ? identity.provider_id : null;
};
@@ -1315,12 +1428,14 @@ const getIdentityValue = (type) => {
* Отслеживает изменения в аутентификации
*/
const watchAuthChanges = () => {
watch(() => auth.isAuthenticated.value, async (newValue, oldValue) => {
watch(
() => auth.isAuthenticated.value,
async (newValue, oldValue) => {
console.log('Изменение аутентификации:', {
from: oldValue,
to: newValue,
userId: auth.userId.value,
authType: auth.authType.value
authType: auth.authType.value,
});
// Обновляем отображение аутентификации
@@ -1330,7 +1445,7 @@ const watchAuthChanges = () => {
address: auth.address.value,
email: auth.email.value,
telegramId: auth.telegramId.value,
telegramUsername: auth.telegramUsername
telegramUsername: auth.telegramUsername,
});
if (newValue && !oldValue) {
@@ -1344,7 +1459,9 @@ const watchAuthChanges = () => {
messageLoading.value.offset = 0;
messageLoading.value.hasMoreMessages = true;
}
}, { immediate: true });
},
{ immediate: true }
);
};
/**
@@ -1356,9 +1473,9 @@ const updateAuthDisplay = (state) => {
if (state.isAuthenticated) {
const authTypeLabels = {
'wallet': 'Кошелек',
'email': 'Email',
'telegram': 'Telegram'
wallet: 'Кошелек',
email: 'Email',
telegram: 'Telegram',
};
let authLabel = authTypeLabels[state.authType] || 'Аккаунт';
@@ -1430,7 +1547,7 @@ const updateBalances = async () => {
eth: balances.eth || '0',
bsc: balances.bsc || '0',
arbitrum: balances.arbitrum || '0',
polygon: balances.polygon || '0'
polygon: balances.polygon || '0',
};
console.log('Обновленные балансы в интерфейсе:', tokenBalances.value);
@@ -1520,15 +1637,20 @@ const handleBlur = () => {
// =====================================================================
// Наблюдаем за изменением адреса кошелька для обновления балансов
watch(() => auth.address?.value, (newAddress) => {
watch(
() => auth.address?.value,
(newAddress) => {
if (newAddress) {
console.log('Адрес кошелька изменился, обновляем балансы');
updateBalances();
}
});
}
);
// Наблюдаем за изменениями в сообщениях для сортировки и прокрутки
watch(() => messages.value.length, (newLength) => {
watch(
() => messages.value.length,
(newLength) => {
if (newLength > 0) {
// Сортируем сообщения по дате/времени
messages.value.sort((a, b) => {
@@ -1542,7 +1664,8 @@ watch(() => messages.value.length, (newLength) => {
scrollToBottom();
});
}
});
}
);
// =====================================================================
// 8. ЖИЗНЕННЫЙ ЦИКЛ КОМПОНЕНТА
@@ -1556,7 +1679,7 @@ onMounted(async () => {
console.log('Состояние аутентификации при загрузке:', {
isAuthenticated: auth.isAuthenticated.value,
authType: auth.authType.value,
telegramId: auth.telegramId.value
telegramId: auth.telegramId.value,
});
// Загружаем сохраненное состояние боковой панели

View File

@@ -45,8 +45,8 @@ export default defineConfig({
changeOrigin: true,
secure: false,
credentials: true,
rewrite: (path) => path
}
}
rewrite: (path) => path,
},
},
},
});

4
yarn.lock Normal file
View File

@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1