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

This commit is contained in:
2025-02-24 18:06:05 +03:00
parent 1f08f07ca5
commit e7a0aacb60
25 changed files with 3299 additions and 1303 deletions

View File

@@ -0,0 +1,238 @@
<template>
<div class="ai-assistant">
<h3>AI Ассистент</h3>
<div class="chat-container" ref="chatContainer">
<div v-if="messages.length === 0" class="empty-state">
Начните диалог с AI ассистентом
</div>
<div v-for="(message, index) in messages" :key="index"
:class="['message', message.role]">
{{ message.content }}
</div>
</div>
<div class="input-container">
<input
v-model="userInput"
@keyup.enter="sendMessage"
placeholder="Введите сообщение..."
:disabled="isLoading"
/>
<button
@click="sendMessage"
:disabled="isLoading || !userInput"
>
{{ isLoading ? 'Отправка...' : 'Отправить' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { getAddress } from 'ethers'
const props = defineProps({
userAddress: String
})
const emit = defineEmits(['chatUpdated'])
const userInput = ref('')
const messages = ref([])
const isLoading = ref(false)
const chatContainer = ref(null)
async function sendMessage() {
if (!userInput.value) return;
try {
isLoading.value = true;
const response = await fetch('http://127.0.0.1:3000/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
message: userInput.value
})
});
if (!response.ok) {
if (response.status === 401) {
messages.value.push({
role: 'system',
content: 'Пожалуйста, подключите кошелек для сохранения истории чата'
});
} else {
throw new Error('Network response was not ok')
}
}
const data = await response.json()
messages.value.push({ role: 'user', content: userInput.value })
messages.value.push({ role: 'assistant', content: data.response })
userInput.value = ''
emit('chatUpdated')
} catch (error) {
console.error('Ошибка при отправке сообщения:', error)
messages.value.push({
role: 'system',
content: 'Произошла ошибка при получении ответа'
})
} finally {
isLoading.value = false
}
}
// Загрузка истории чата
async function loadChatHistory() {
try {
if (!props.userAddress) {
console.log('Адрес пользователя не определен');
return;
}
const response = await fetch('http://127.0.0.1:3000/api/chat/history', {
credentials: 'include'
});
if (!response.ok) {
const error = await response.json();
console.log('Ошибка загрузки истории:', error);
if (error.error === 'User not found') {
// Создаем нового пользователя
const createUserResponse = await fetch('http://127.0.0.1:3000/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
address: props.userAddress
})
});
if (!createUserResponse.ok) {
console.error('Ошибка создания пользователя:', await createUserResponse.text());
return;
}
if (createUserResponse.ok) {
// Повторно загружаем историю
return loadChatHistory();
}
}
return;
}
const data = await response.json();
messages.value = (data.history || []).map(chat => ({
role: chat.is_user ? 'user' : 'assistant',
content: chat.is_user ? chat.message : JSON.parse(chat.response).content
}));
} catch (error) {
console.error('Ошибка загрузки истории:', error);
}
}
// Загружаем историю при монтировании
onMounted(() => {
if (props.userAddress) {
loadChatHistory()
}
})
// Добавляем наблюдение за изменением адреса
watch(() => props.userAddress, (newAddress) => {
if (newAddress) {
loadChatHistory()
} else {
messages.value = []
}
})
watch(messages, () => {
if (chatContainer.value) {
setTimeout(() => {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
}, 0)
}
}, { deep: true })
</script>
<style scoped>
.empty-state {
text-align: center;
color: #666;
padding: 2rem;
}
.ai-assistant {
margin-top: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.chat-container {
height: 300px;
overflow-y: auto;
margin-bottom: 15px;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
}
.message {
margin: 8px 0;
padding: 8px 12px;
border-radius: 4px;
}
.user {
background-color: #e3f2fd;
margin-left: 20%;
}
.assistant {
background-color: #f5f5f5;
margin-right: 20%;
}
.system {
background-color: #ffebee;
text-align: center;
}
.input-container {
display: flex;
gap: 10px;
}
input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #cccccc;
}
</style>

View File

@@ -0,0 +1,281 @@
<template>
<div class="data-tables">
<h3>Данные из базы</h3>
<!-- История чатов -->
<div class="table-section">
<h4>История чатов</h4>
<div v-if="displayedChats.length === 0" class="empty-state">
Нет доступных сообщений
</div>
<table v-else>
<thead>
<tr>
<th>Адрес</th>
<th>Сообщение</th>
<th>Ответ</th>
<th>Дата</th>
<th v-if="isAdmin">Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="chat in displayedChats" :key="chat.id">
<td>{{ shortenAddress(chat.address) }}</td>
<td>{{ chat.message }}</td>
<td>{{ JSON.parse(chat.response).content }}</td>
<td>{{ formatDate(chat.created_at) }}</td>
<td v-if="isAdmin">
<button
@click="approveChat(chat.id)"
:disabled="chat.is_approved"
:class="{ approved: chat.is_approved }"
>
{{ chat.is_approved ? 'Одобрен' : 'Одобрить' }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Пользователи -->
<div class="table-section">
<h4>Пользователи</h4>
<table>
<thead>
<tr>
<th>ID</th>
<th>Адрес</th>
<th>Дата регистрации</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ shortenAddress(user.address) }}</td>
<td>{{ formatDate(user.created_at) }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="isAdmin" class="chat-history">
<h2>История сообщений всех пользователей</h2>
<table>
<thead>
<tr>
<th>Адрес</th>
<th>Сообщение</th>
<th>Ответ</th>
<th>Дата</th>
</tr>
</thead>
<tbody>
<tr v-for="chat in allChats" :key="chat.id">
<td>{{ shortenAddress(chat.address) }}</td>
<td>{{ chat.message }}</td>
<td>{{ JSON.parse(chat.response).content }}</td>
<td>{{ new Date(chat.created_at).toLocaleString() }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import contractABI from '../../artifacts/MyContract.json'
const props = defineProps({
isConnected: Boolean,
userAddress: String
});
const allChats = ref([]);
const users = ref([]);
const isAdmin = ref(false);
const displayedChats = computed(() => {
return isAdmin.value ? allChats.value : allChats.value.filter(
chat => chat.address.toLowerCase() === props.userAddress?.toLowerCase()
);
});
// Нормализация адреса (приведение к нижнему регистру)
function normalizeAddress(address) {
return address?.toLowerCase() || '';
}
// Следим за изменением состояния подключения
watch(() => props.isConnected, (newValue) => {
console.log('isConnected изменился:', newValue);
if (newValue) {
// Небольшая задержка для обновления сессии
setTimeout(() => {
fetchData();
}, 500);
}
});
// Следим за изменением адреса
watch(() => props.userAddress, (newValue) => {
console.log('userAddress изменился:', newValue);
if (props.isConnected && newValue) {
fetchData();
}
});
// Получение данных
async function fetchData() {
try {
console.log('Запрос обновления данных');
if (!props.userAddress) {
console.log('Адрес пользователя не определен');
return;
}
// Проверяем права админа
const adminCheck = await fetch('http://127.0.0.1:3000/api/admin/check', {
credentials: 'include'
});
if (adminCheck.ok) {
const { isAdmin: adminStatus } = await adminCheck.json();
isAdmin.value = adminStatus;
console.log('Статус админа:', adminStatus);
}
// Получаем чаты
const chatsResponse = await fetch('http://127.0.0.1:3000/api/admin/chats', {
credentials: 'include'
});
if (chatsResponse.ok) {
const data = await chatsResponse.json();
allChats.value = data.chats || [];
}
// Получаем пользователей
const usersResponse = await fetch('http://127.0.0.1:3000/api/users', {
credentials: 'include'
});
if (usersResponse.ok) {
const data = await usersResponse.json();
users.value = data.users || [];
}
} catch (error) {
console.error('Ошибка получения данных:', error);
}
}
// Форматирование адреса
function shortenAddress(address) {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
// Форматирование даты
function formatDate(date) {
return new Date(date).toLocaleString();
}
async function approveChat(chatId) {
try {
const response = await fetch('http://127.0.0.1:3000/api/admin/approve', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ chatId })
});
if (!response.ok) throw new Error('Failed to approve chat');
// Обновляем статус локально
const chat = allChats.value.find(c => c.id === chatId);
if (chat) chat.is_approved = true;
} catch (error) {
console.error('Error approving chat:', error);
alert('Ошибка при одобрении чата');
}
}
onMounted(() => {
if (props.isConnected) {
fetchData();
}
});
// Делаем метод доступным извне
defineExpose({
fetchData
});
async function fetchAllChats() {
try {
const response = await fetch('http://127.0.0.1:3000/api/admin/chats', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
allChats.value = data.chats;
}
} catch (error) {
console.error('Ошибка получения чатов:', error);
}
}
onMounted(() => {
if (props.isAdmin) {
fetchAllChats();
}
});
watch(() => props.isAdmin, (newValue) => {
if (newValue) {
fetchAllChats();
}
});
</script>
<style scoped>
.data-tables {
margin: 20px;
}
.table-section {
margin-bottom: 30px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f5f5f5;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
.empty-state {
text-align: center;
color: #666;
padding: 1rem;
background: #f8f9fa;
border-radius: 4px;
margin: 1rem 0;
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<div class="vector-store">
<h2>Векторное хранилище</h2>
<div v-if="loading" class="loading">
Загрузка...
</div>
<div v-else>
<div class="stats">
<p>Всего документов: {{ vectors.length }}</p>
<p>Размерность эмбеддингов: {{ getEmbeddingStats }}</p>
</div>
<div class="filters">
<input
v-model="search"
placeholder="Поиск по содержанию..."
class="search-input"
>
<select v-model="typeFilter" class="type-filter">
<option value="">Все типы</option>
<option value="approved_chat">Одобренные чаты</option>
</select>
</div>
<table class="vectors-table">
<thead>
<tr>
<th>ID</th>
<th>Содержание</th>
<th>Метаданные</th>
<th>Дата создания</th>
</tr>
</thead>
<tbody>
<tr v-for="vector in filteredVectors" :key="vector.id">
<td>{{ vector.id }}</td>
<td class="content-cell">
<div class="qa-format">
<div class="question">Q: {{ getQuestion(vector.content) }}</div>
<div class="answer">A: {{ getAnswer(vector.content) }}</div>
</div>
</td>
<td>
<div class="metadata">
<span class="metadata-item">
<strong>Тип:</strong> {{ vector.metadata.type }}
</span>
<span class="metadata-item">
<strong>Одобрил:</strong> {{ shortenAddress(vector.metadata.approvedBy) }}
</span>
<span class="metadata-item">
<strong>ID чата:</strong> {{ vector.metadata.chatId }}
</span>
</div>
</td>
<td>{{ formatDate(vector.created) }}</td>
</tr>
</tbody>
</table>
<div v-if="error" class="error-message">
{{ error }}
</div>
</div>
</div>
</template>
<script>
const API_BASE_URL = 'http://localhost:3000';
export default {
name: 'VectorStore',
data() {
return {
vectors: [],
loading: true,
error: null,
search: '',
typeFilter: '',
baseUrl: API_BASE_URL
}
},
computed: {
getEmbeddingStats() {
if (!this.vectors.length) return 'Нет данных';
const sizes = this.vectors.map(v => v.embedding_size);
const uniqueSizes = [...new Set(sizes)];
if (uniqueSizes.length === 1) {
return uniqueSizes[0];
}
return `Разные размерности: ${uniqueSizes.join(', ')}`;
},
filteredVectors() {
return this.vectors.filter(vector => {
const matchesSearch = this.search === '' ||
vector.content.toLowerCase().includes(this.search.toLowerCase());
const matchesType = this.typeFilter === '' ||
vector.metadata.type === this.typeFilter;
return matchesSearch && matchesType;
});
}
},
async mounted() {
await this.loadVectors();
},
methods: {
async loadVectors() {
try {
const response = await fetch('http://127.0.0.1:3000/api/admin/vectors', {
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
if (response.status === 401) {
this.error = 'Необходима авторизация. Пожалуйста, подключите кошелек.';
return;
}
if (response.status === 403) {
this.error = 'Доступ запрещен. Только владелец контракта может просматривать векторное хранилище.';
return;
}
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
if (!Array.isArray(data.vectors)) {
throw new Error('Неверный формат данных');
}
this.vectors = data.vectors;
this.error = null;
} catch (error) {
console.error('Error loading vectors:', error);
this.error = `Ошибка загрузки векторов: ${error.message}.
${error.message.includes('404') ?
'API эндпоинт не найден. Проверьте URL и работу сервера.' :
'Проверьте права доступа и подключение к серверу.'}`;
} finally {
this.loading = false;
}
},
getQuestion(content) {
return content.split('\nA:')[0].replace('Q:', '').trim();
},
getAnswer(content) {
return content.split('\nA:')[1]?.trim() || '';
},
shortenAddress(address) {
return address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '';
},
formatDate(date) {
return new Date(date).toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
}
}
</script>
<style scoped>
.vector-store {
padding: 20px;
}
.filters {
margin: 20px 0;
display: flex;
gap: 10px;
}
.search-input,
.type-filter {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.search-input {
flex: 1;
}
.vectors-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.vectors-table th,
.vectors-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.vectors-table th {
background-color: #f5f5f5;
}
.content-cell {
max-width: 400px;
}
.qa-format {
white-space: pre-wrap;
}
.question {
color: #2c3e50;
margin-bottom: 5px;
}
.answer {
color: #34495e;
}
.metadata {
display: flex;
flex-direction: column;
gap: 5px;
}
.metadata-item {
background: #f8f9fa;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9em;
}
.stats {
margin: 20px 0;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}
.error-message {
margin-top: 20px;
padding: 10px;
background-color: #fee;
border: 1px solid #fcc;
border-radius: 4px;
color: #c00;
}
</style>

View File

@@ -1,240 +0,0 @@
<template>
<div class="ai-assistant" v-if="isConnected">
<h3>AI Ассистент</h3>
<div class="chat-container" ref="chatContainer">
<div v-for="(message, index) in messages" :key="index"
:class="['message', message.role]">
{{ message.content }}
</div>
</div>
<div class="input-container">
<input
v-model="userInput"
@keyup.enter="sendMessage"
placeholder="Задайте вопрос..."
:disabled="isLoading"
/>
<button
@click="sendMessage"
:disabled="!userInput || isLoading"
>
{{ isLoading ? 'Отправка...' : 'Отправить' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { getAddress } from 'ethers'
const props = defineProps({
isConnected: Boolean,
userAddress: String
})
const emit = defineEmits(['chatUpdated'])
const userInput = ref('')
const messages = ref([])
const isLoading = ref(false)
const isAuthenticated = ref(false)
const chatContainer = ref(null)
// Функция для SIWE аутентификации
async function authenticateWithSIWE() {
try {
// Получаем nonce
const nonceResponse = await fetch(
'http://127.0.0.1:3000/nonce',
{ credentials: 'include' }
);
const { nonce } = await nonceResponse.json();
// Создаем сообщение для подписи
const message =
`127.0.0.1:5174 wants you to sign in with your Ethereum account:\n` +
`${getAddress(props.userAddress)}\n\n` +
`Sign in with Ethereum to access AI Assistant\n\n` +
`URI: http://127.0.0.1:5174\n` +
`Version: 1\n` +
`Chain ID: 11155111\n` +
`Nonce: ${nonce}\n` +
`Issued At: ${new Date().toISOString()}\n` +
`Resources:\n` +
`- http://127.0.0.1:5174/api/chat`;
// Получаем подпись
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, props.userAddress]
});
// Верифицируем подпись
const verifyResponse = await fetch('http://127.0.0.1:3000/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-SIWE-Nonce': nonce
},
credentials: 'include',
body: JSON.stringify({ message, signature })
});
if (!verifyResponse.ok) {
throw new Error('Failed to verify signature');
}
isAuthenticated.value = true;
} catch (error) {
console.error('SIWE error:', error);
messages.value.push({
role: 'system',
content: 'Ошибка аутентификации. Пожалуйста, попробуйте снова.'
});
}
}
async function sendMessage() {
if (!userInput.value) return;
try {
isLoading.value = true;
const response = await fetch('http://127.0.0.1:3000/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
message: userInput.value,
userAddress: props.userAddress
})
});
if (response.status === 401) {
messages.value.push({
role: 'system',
content: 'Пожалуйста, подключите кошелек и авторизуйтесь'
});
return;
}
if (!response.ok) {
throw new Error('Network response was not ok')
}
const data = await response.json()
messages.value.push({ role: 'user', content: userInput.value })
messages.value.push({ role: 'assistant', content: data.response })
userInput.value = ''
emit('chatUpdated')
} catch (error) {
console.error('Ошибка при отправке сообщения:', error)
messages.value.push({
role: 'system',
content: 'Произошла ошибка при получении ответа'
})
} finally {
isLoading.value = false
}
}
// Загрузка истории чата
async function loadChatHistory() {
try {
const response = await fetch('http://127.0.0.1:3000/api/chat/history', {
credentials: 'include'
});
const data = await response.json();
messages.value = data.history.map(item => ({
role: item.is_user ? 'user' : 'assistant',
content: item.is_user ? item.message : item.response
}));
} catch (error) {
console.error('Ошибка загрузки истории:', error);
}
}
// Загружаем историю при монтировании
onMounted(() => {
if (props.isConnected) {
loadChatHistory();
}
});
watch(messages, () => {
if (chatContainer.value) {
setTimeout(() => {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
}, 0)
}
}, { deep: true })
</script>
<style scoped>
.ai-assistant {
margin-top: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.chat-container {
height: 300px;
overflow-y: auto;
margin-bottom: 15px;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
}
.message {
margin: 8px 0;
padding: 8px 12px;
border-radius: 4px;
}
.user {
background-color: #e3f2fd;
margin-left: 20%;
}
.assistant {
background-color: #f5f5f5;
margin-right: 20%;
}
.system {
background-color: #ffebee;
text-align: center;
}
.input-container {
display: flex;
gap: 10px;
}
input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #cccccc;
}
</style>

View File

@@ -0,0 +1,162 @@
<template>
<div class="contract-deploy">
<h2>Деплой смарт-контракта</h2>
<form @submit.prevent="deployContract" class="deploy-form">
<div class="form-group">
<label>Начальная цена (ETH)</label>
<input
v-model="initialPrice"
type="number"
step="0.001"
min="0"
required
class="form-input"
/>
</div>
<div class="form-group">
<label>Название токена</label>
<input
v-model="tokenName"
type="text"
required
class="form-input"
/>
</div>
<div class="form-group">
<label>Символ токена</label>
<input
v-model="tokenSymbol"
type="text"
required
class="form-input"
/>
</div>
<button
type="submit"
:disabled="isDeploying"
class="deploy-button"
>
{{ isDeploying ? 'Деплой...' : 'Деплой контракта' }}
</button>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="deployedAddress" class="success-message">
Контракт развернут по адресу: {{ deployedAddress }}
</div>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ethers } from 'ethers'
import contractABI from '../../artifacts/MyContract.json'
const initialPrice = ref(0.01)
const tokenName = ref('')
const tokenSymbol = ref('')
const isDeploying = ref(false)
const error = ref('')
const deployedAddress = ref('')
async function deployContract() {
if (!window.ethereum) {
error.value = 'MetaMask не установлен'
return
}
try {
isDeploying.value = true
error.value = ''
const provider = new ethers.BrowserProvider(window.ethereum)
const signer = await provider.getSigner()
// Создаем фабрику контракта
const factory = new ethers.ContractFactory(
contractABI.abi,
contractABI.bytecode,
signer
)
// Деплоим контракт
const contract = await factory.deploy(
ethers.parseEther(initialPrice.value.toString()),
tokenName.value,
tokenSymbol.value
)
await contract.waitForDeployment()
deployedAddress.value = await contract.getAddress()
} catch (err) {
console.error('Ошибка деплоя:', err)
error.value = err.message
} finally {
isDeploying.value = false
}
}
</script>
<style scoped>
.contract-deploy {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.deploy-form {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #2c3e50;
}
.form-input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.deploy-button {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.deploy-button:disabled {
background-color: #cccccc;
}
.error-message {
color: #dc3545;
margin-top: 10px;
}
.success-message {
color: #28a745;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,771 @@
<template>
<div class="contract-interaction">
<h2>Взаимодействие с контрактом</h2>
<div v-if="!isInitialized" class="loading-message">
Загрузка контракта...
</div>
<div v-if="error" class="error-message">{{ error }}</div>
<div v-if="isCorrectNetwork" class="wallet-info">
<p>Адрес кошелька: {{ address }}</p>
<div class="contract-controls">
<h3>Управление контрактом</h3>
<p v-if="currentPrice" class="price-info">
Текущая цена: {{ formatPrice(currentPrice) }} ETH
</p>
<!-- Панель управления для владельца -->
<div v-if="isOwner" class="owner-controls">
<h4>Панель владельца</h4>
<div class="input-group">
<input
v-model="newPrice"
type="number"
step="0.001"
placeholder="Новая цена (ETH)"
class="amount-input"
/>
<button
@click="handleSetPrice"
:disabled="!newPrice || isLoading"
class="admin-button"
>
Установить цену
</button>
</div>
<button
@click="handleWithdraw"
:disabled="isLoading"
class="admin-button withdraw-button"
>
Вывести средства
</button>
</div>
<!-- Панель покупки -->
<div class="purchase-panel">
<h4>Покупка</h4>
<div class="input-group">
<input
v-model="amount"
type="number"
placeholder="Введите количество"
class="amount-input"
/>
<button
@click="handlePurchase"
:disabled="!amount || isLoading"
class="purchase-button"
>
{{ isLoading ? 'Обработка...' : 'Купить' }}
</button>
</div>
<p v-if="amount && currentPrice" class="total-cost">
Общая стоимость: {{ formatPrice(calculateTotalCost()) }} ETH
</p>
</div>
<p v-if="success" class="success-message">{{ success }}</p>
</div>
</div>
<h3>Управление смарт-контрактом</h3>
<div class="contract-info">
<p>Адрес контракта: {{ contractAddress }}</p>
<p>Начальная цена: {{ initialPrice }} ETH</p>
<p>Владелец: {{ contractOwner }}</p>
</div>
<div class="contract-actions">
<button
v-if="isOwner"
@click="withdrawFunds"
:disabled="withdrawing"
>
{{ withdrawing ? 'Вывод средств...' : 'Вывести средства' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { BrowserProvider, Contract, JsonRpcProvider, formatEther, parseEther, getAddress } from 'ethers'
import { ethers } from 'ethers'
const props = defineProps({
userAddress: String
})
// Инициализируем все ref переменные в начале
const address = ref('')
const currentPrice = ref(null)
const contractOwner = ref(null)
const amount = ref(1)
const newPrice = ref('')
const isLoading = ref(false)
const error = ref('')
const success = ref('')
const isCorrectNetwork = ref(false)
const isConnected = ref(false)
const isInitialized = ref(false)
const walletProvider = ref(null)
const isAuthenticated = ref(false)
const statement = 'Sign in with Ethereum to access DApp features and AI Assistant'
const initialPrice = ref('0.009')
const contractAddress = '0x9acac5926e86fE2d7A69deff27a4b2D310108d76' // Адрес из логов сервера
const withdrawing = ref(false)
// Константы
const SEPOLIA_CHAIN_ID = 11155111
const provider = new JsonRpcProvider(import.meta.env.VITE_APP_ETHEREUM_NETWORK_URL)
const contractABI = [
{
"inputs": [{"internalType": "uint256", "name": "amount", "type": "uint256"}],
"name": "purchase",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "price",
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"internalType": "uint256", "name": "newPrice", "type": "uint256"}],
"name": "setPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{"indexed": false, "internalType": "address", "name": "buyer", "type": "address"},
{"indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "Purchase",
"type": "event"
}
]
// Вычисляемые свойства
const isOwner = computed(() => {
return address.value && contractOwner.value &&
address.value.toLowerCase() === contractOwner.value.toLowerCase()
})
// Функции
function formatPrice(price) {
if (!price) return '0'
return formatEther(price)
}
// Функция инициализации контракта
async function initializeContract() {
try {
if (!contractAddress) {
throw new Error('Contract address not configured')
}
const provider = new JsonRpcProvider(import.meta.env.VITE_APP_ETHEREUM_NETWORK_URL)
const contract = new Contract(
contractAddress,
contractABI,
provider
)
await Promise.all([
contract.price().then(price => {
currentPrice.value = price
console.log('Начальная цена:', formatEther(price), 'ETH')
}),
contract.owner().then(owner => {
contractOwner.value = owner
console.log('Владелец контракта:', owner)
})
])
isInitialized.value = true
} catch (err) {
console.error('Ошибка при инициализации контракта:', err)
error.value = 'Ошибка при инициализации контракта: ' + err.message
}
}
// Функция подключения к MetaMask
async function connectWallet() {
try {
// Проверяем доступность MetaMask
if (!window.ethereum) {
throw new Error('MetaMask не установлен')
}
// Запрашиваем доступ к аккаунту
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
})
// Сохраняем адрес и провайдер
address.value = accounts[0]
walletProvider.value = window.ethereum
isConnected.value = true
// SIWE аутентификация
try {
// Получаем nonce
const nonceResponse = await fetch(
'http://127.0.0.1:3000/nonce',
{ credentials: 'include' }
);
const { nonce } = await nonceResponse.json();
// Сохраняем nonce в localStorage
localStorage.setItem('siwe-nonce', nonce);
// Создаем сообщение для подписи
const message =
`${window.location.host} wants you to sign in with your Ethereum account:\n` +
`${getAddress(address.value)}\n\n` +
`${statement}\n\n` +
`URI: http://${window.location.host}\n` +
`Version: 1\n` +
`Chain ID: 11155111\n` +
`Nonce: ${nonce}\n` +
`Issued At: ${new Date().toISOString()}\n` +
`Resources:\n` +
`- http://${window.location.host}/api/chat\n` +
`- http://${window.location.host}/api/contract`;
// Получаем подпись
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, address.value]
});
// Верифицируем подпись
const verifyResponse = await fetch(
'http://127.0.0.1:3000/verify',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-SIWE-Nonce': nonce
},
credentials: 'include',
body: JSON.stringify({ message, signature })
}
);
if (!verifyResponse.ok) {
throw new Error('Failed to verify signature');
}
} catch (error) {
console.error('SIWE error:', error);
throw new Error('Ошибка аутентификации');
}
// Подписываемся на изменение аккаунта
window.ethereum.on('accountsChanged', handleAccountsChanged)
window.ethereum.on('chainChanged', handleChainChanged)
await checkNetwork()
} catch (err) {
console.error('Ошибка при подключении кошелька:', err)
error.value = 'Ошибка при подключении кошелька: ' + err.message
}
}
// Обработчики событий MetaMask
function handleAccountsChanged(accounts) {
if (accounts.length === 0) {
// MetaMask отключен
isConnected.value = false
address.value = null
} else {
// Аккаунт изменен
address.value = accounts[0]
}
}
function handleChainChanged() {
// При смене сети перезагружаем страницу
window.location.reload()
}
// Обновляем watch
watch(isConnected, async (newValue) => {
try {
if (newValue) {
console.log('Кошелек подключен, адрес:', address.value)
if (!isInitialized.value) {
await initializeContract()
}
await checkNetwork()
} else {
console.log('Кошелек отключен')
currentPrice.value = null
contractOwner.value = null
isCorrectNetwork.value = false
error.value = ''
isInitialized.value = false
}
} catch (err) {
console.error('Ошибка при обработке подключения:', err)
error.value = 'Ошибка при обработке подключения: ' + err.message
}
}, { immediate: true })
// Обновляем функцию проверки сети
async function checkNetwork() {
try {
if (!walletProvider.value) {
isCorrectNetwork.value = false
error.value = 'Провайдер кошелька недоступен'
return
}
const ethersProvider = new BrowserProvider(walletProvider.value)
const network = await ethersProvider.getNetwork()
console.log('Текущая сеть:', network.chainId)
isCorrectNetwork.value = Number(network.chainId) === SEPOLIA_CHAIN_ID
if (!isCorrectNetwork.value) {
error.value = `Пожалуйста, переключитесь на сеть Sepolia (${SEPOLIA_CHAIN_ID}). Текущая сеть: ${network.chainId}`
} else {
error.value = ''
await Promise.all([fetchPrice(), fetchOwner()])
}
} catch (err) {
console.error('Ошибка при проверке сети:', err)
isCorrectNetwork.value = false
error.value = 'Ошибка при проверке сети: ' + err.message
}
}
// Обновляем функцию fetchPrice с обработкой ошибок
async function fetchPrice() {
try {
const contract = new Contract(contractAddress, contractABI, provider)
const price = await contract.price()
currentPrice.value = price
console.log('Текущая цена:', formatEther(price), 'ETH')
} catch (err) {
console.error('Ошибка при получении цены:', err)
error.value = `Не удалось получить текущую цену: ${err.message}`
currentPrice.value = null
}
}
// Получение адреса владельца
async function fetchOwner() {
try {
const contract = new Contract(contractAddress, contractABI, provider)
contractOwner.value = await contract.owner()
} catch (err) {
console.error('Ошибка при получении адреса владельца:', err)
}
}
// Обновляем onMounted
onMounted(async () => {
console.log('Компонент смонтирован')
try {
await initializeContract()
if (provider) {
const contract = new Contract(contractAddress, contractABI, provider)
contract.on('Purchase', (buyer, amount) => {
console.log(`Новая покупка: ${amount} единиц от ${buyer}`)
fetchPrice()
})
return () => {
contract.removeAllListeners('Purchase')
}
}
} catch (err) {
console.error('Ошибка при монтировании компонента:', err)
}
})
// Добавляем функцию для расчета общей стоимости
function calculateTotalCost() {
if (!currentPrice.value || !amount.value) return BigInt(0)
return currentPrice.value * BigInt(amount.value)
}
// Обновляем handlePurchase
async function handlePurchase() {
if (!amount.value) return
if (!isCorrectNetwork.value) {
error.value = 'Пожалуйста, переключитесь на сеть Sepolia'
return
}
error.value = ''
success.value = ''
try {
isLoading.value = true
const ethersProvider = new BrowserProvider(walletProvider.value)
const signer = await ethersProvider.getSigner()
const contract = new Contract(contractAddress, contractABI, signer)
const totalCost = calculateTotalCost()
// Проверяем баланс
const balance = await ethersProvider.getBalance(await signer.getAddress())
console.log('Баланс кошелька:', formatEther(balance), 'ETH')
if (balance < totalCost) {
throw new Error(`Недостаточно средств. Нужно ${formatEther(totalCost)} ETH`)
}
console.log('Общая стоимость:', formatEther(totalCost), 'ETH')
console.log('Параметры транзакции:', {
amount: amount.value,
totalCost: formatEther(totalCost),
from: await signer.getAddress()
})
const tx = await contract.purchase(amount.value, {
value: totalCost,
gasLimit: 100000 // Явно указываем лимит газа
})
console.log('Транзакция отправлена:', tx.hash)
await tx.wait()
console.log('Транзакция подтверждена')
amount.value = ''
success.value = 'Покупка успешно совершена!'
await fetchPrice()
} catch (err) {
console.error('Ошибка при покупке:', err)
console.error('Детали ошибки:', {
code: err.code,
message: err.message,
data: err.data,
reason: err.reason
})
if (err.message.includes('user rejected')) {
error.value = 'Транзакция отменена пользователем'
} else if (err.message.includes('Недостаточно средств')) {
error.value = err.message
} else {
error.value = 'Произошла ошибка при совершении покупки: ' + err.message
}
} finally {
isLoading.value = false
}
}
// Добавляем новые функции
async function handleSetPrice() {
if (!newPrice.value) return
error.value = ''
success.value = ''
try {
isLoading.value = true
const ethersProvider = new BrowserProvider(walletProvider.value)
const signer = await ethersProvider.getSigner()
const contract = new Contract(contractAddress, contractABI, signer)
const priceInWei = parseEther(newPrice.value.toString())
const tx = await contract.setPrice(priceInWei)
await tx.wait()
newPrice.value = ''
success.value = 'Цена успешно обновлена!'
await fetchPrice()
} catch (err) {
console.error('Ошибка при установке цены:', err)
error.value = 'Ошибка при установке цены: ' + err.message
} finally {
isLoading.value = false
}
}
async function handleWithdraw() {
error.value = ''
success.value = ''
try {
isLoading.value = true
const ethersProvider = new BrowserProvider(walletProvider.value)
const signer = await ethersProvider.getSigner()
const contract = new Contract(contractAddress, contractABI, signer)
const tx = await contract.withdraw()
await tx.wait()
success.value = 'Средства успешно выведены!'
} catch (err) {
console.error('Ошибка при выводе средств:', err)
error.value = 'Ошибка при выводе средств: ' + err.message
} finally {
isLoading.value = false
}
}
async function disconnectWallet() {
try {
// Выходим из системы на сервере
const response = await fetch('http://127.0.0.1:3000/api/signout', {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to sign out');
}
// Отключаем кошелек
if (window.ethereum) {
await window.ethereum.request({
method: 'wallet_requestPermissions',
params: [{ eth_accounts: {} }]
});
}
// Сбрасываем состояние
isConnected.value = false;
address.value = '';
// Перезагружаем страницу для очистки состояния
window.location.reload();
} catch (error) {
console.error('Error disconnecting wallet:', error);
alert('Ошибка при отключении кошелька');
}
}
defineExpose({
isConnected,
address
})
</script>
<style scoped>
.contract-interaction {
padding: 20px;
max-width: 600px;
margin: 0 auto;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.wallet-info {
margin-top: 20px;
padding: 15px;
background-color: #f5f5f5;
border-radius: 8px;
}
.contract-controls {
margin-top: 20px;
}
.input-group {
display: flex;
gap: 10px;
margin-top: 10px;
}
.amount-input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.purchase-button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.purchase-button:hover:not(:disabled) {
background-color: #45a049;
}
.purchase-button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.error-message {
color: #dc3545;
padding: 10px;
margin: 10px 0;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
.price-info {
margin: 10px 0;
font-weight: bold;
color: #2c3e50;
}
.owner-controls {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.purchase-panel {
margin-top: 20px;
}
.admin-button {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.admin-button:hover:not(:disabled) {
background-color: #0056b3;
}
.withdraw-button {
margin-top: 10px;
background-color: #6c757d;
}
.withdraw-button:hover:not(:disabled) {
background-color: #5a6268;
}
.total-cost {
margin-top: 10px;
font-size: 0.9em;
color: #6c757d;
}
.success-message {
color: #28a745;
margin-top: 10px;
font-size: 0.9em;
}
.loading-message {
color: #6c757d;
text-align: center;
padding: 20px;
background-color: #f8f9fa;
border-radius: 4px;
margin: 10px 0;
}
.auth-status {
margin: 10px 0;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.authenticated {
color: #28a745;
}
.signout-button {
padding: 5px 10px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.signout-button:hover {
background-color: #c82333;
}
.connect-button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.connect-button:hover {
background-color: #45a049;
}
.wallet-status {
display: flex;
justify-content: flex-end;
margin-bottom: 1rem;
}
.disconnect-btn {
background-color: #dc3545;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.disconnect-btn:hover {
background-color: #c82333;
}
.contract-info {
text-align: left;
margin-bottom: 1rem;
}
.contract-actions {
margin-top: 1rem;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -1,685 +0,0 @@
<template>
<div class="contract-interaction">
<h2>Взаимодействие с контрактом</h2>
<div v-if="!isInitialized" class="loading-message">
Загрузка контракта...
</div>
<div v-if="!isConnected">
<button
@click="connectWallet"
class="connect-button"
>
Подключить кошелек
</button>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
<div v-if="isConnected && isCorrectNetwork" class="wallet-info">
<p>Адрес кошелька: {{ address }}</p>
<div class="contract-controls">
<h3>Управление контрактом</h3>
<p v-if="currentPrice" class="price-info">
Текущая цена: {{ formatPrice(currentPrice) }} ETH
</p>
<!-- Панель управления для владельца -->
<div v-if="isOwner" class="owner-controls">
<h4>Панель владельца</h4>
<div class="input-group">
<input
v-model="newPrice"
type="number"
step="0.001"
placeholder="Новая цена (ETH)"
class="amount-input"
/>
<button
@click="handleSetPrice"
:disabled="!newPrice || isLoading"
class="admin-button"
>
Установить цену
</button>
</div>
<button
@click="handleWithdraw"
:disabled="isLoading"
class="admin-button withdraw-button"
>
Вывести средства
</button>
</div>
<!-- Панель покупки -->
<div class="purchase-panel">
<h4>Покупка</h4>
<div class="input-group">
<input
v-model="amount"
type="number"
placeholder="Введите количество"
class="amount-input"
/>
<button
@click="handlePurchase"
:disabled="!amount || isLoading"
class="purchase-button"
>
{{ isLoading ? 'Обработка...' : 'Купить' }}
</button>
</div>
<p v-if="amount && currentPrice" class="total-cost">
Общая стоимость: {{ formatPrice(calculateTotalCost()) }} ETH
</p>
</div>
<p v-if="success" class="success-message">{{ success }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { BrowserProvider, Contract, JsonRpcProvider, formatEther, parseEther, getAddress } from 'ethers'
// Инициализируем все ref переменные в начале
const amount = ref('')
const newPrice = ref('')
const isLoading = ref(false)
const error = ref('')
const success = ref('')
const currentPrice = ref(null)
const contractOwner = ref(null)
const isCorrectNetwork = ref(false)
const isConnected = ref(false)
const isInitialized = ref(false)
const address = ref(null)
const walletProvider = ref(null)
const isAuthenticated = ref(false)
const statement = 'Sign in with Ethereum to access DApp features and AI Assistant'
// Константы
const SEPOLIA_CHAIN_ID = 11155111
const provider = new JsonRpcProvider(import.meta.env.VITE_APP_ETHEREUM_NETWORK_URL)
const contractAddress = '0xFF7602583E82C097Ae548Fc8B894F0a73089985E'
const contractABI = [
{
"inputs": [{"internalType": "uint256", "name": "amount", "type": "uint256"}],
"name": "purchase",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "price",
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"internalType": "uint256", "name": "newPrice", "type": "uint256"}],
"name": "setPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{"indexed": false, "internalType": "address", "name": "buyer", "type": "address"},
{"indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "Purchase",
"type": "event"
}
]
// Вычисляемые свойства
const isOwner = computed(() => {
return address.value && contractOwner.value &&
address.value.toLowerCase() === contractOwner.value.toLowerCase()
})
// Функции
function formatPrice(price) {
if (!price) return '0'
return formatEther(price)
}
// Функция инициализации контракта
async function initializeContract() {
try {
if (!provider) {
throw new Error('Provider не доступен')
}
const contract = new Contract(contractAddress, contractABI, provider)
await Promise.all([
contract.price().then(price => {
currentPrice.value = price
console.log('Начальная цена:', formatEther(price), 'ETH')
}),
contract.owner().then(owner => {
contractOwner.value = owner
console.log('Владелец контракта:', owner)
})
])
isInitialized.value = true
} catch (err) {
console.error('Ошибка при инициализации контракта:', err)
error.value = 'Ошибка при инициализации контракта: ' + err.message
}
}
// Функция подключения к MetaMask
async function connectWallet() {
try {
// Проверяем доступность MetaMask
if (!window.ethereum) {
throw new Error('MetaMask не установлен')
}
// Запрашиваем доступ к аккаунту
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
})
// Сохраняем адрес и провайдер
address.value = accounts[0]
walletProvider.value = window.ethereum
isConnected.value = true
// SIWE аутентификация
try {
// Получаем nonce
const nonceResponse = await fetch(
'http://127.0.0.1:3000/nonce',
{ credentials: 'include' }
);
const { nonce } = await nonceResponse.json();
// Сохраняем nonce в localStorage
localStorage.setItem('siwe-nonce', nonce);
// Создаем сообщение для подписи
const message =
`${window.location.host} wants you to sign in with your Ethereum account:\n` +
`${getAddress(address.value)}\n\n` +
`${statement}\n\n` +
`URI: http://${window.location.host}\n` +
`Version: 1\n` +
`Chain ID: 11155111\n` +
`Nonce: ${nonce}\n` +
`Issued At: ${new Date().toISOString()}\n` +
`Resources:\n` +
`- http://${window.location.host}/api/chat\n` +
`- http://${window.location.host}/api/contract`;
// Получаем подпись
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, address.value]
});
// Верифицируем подпись
const verifyResponse = await fetch(
'http://127.0.0.1:3000/verify',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-SIWE-Nonce': nonce
},
credentials: 'include',
body: JSON.stringify({ message, signature })
}
);
if (!verifyResponse.ok) {
throw new Error('Failed to verify signature');
}
} catch (error) {
console.error('SIWE error:', error);
throw new Error('Ошибка аутентификации');
}
// Подписываемся на изменение аккаунта
window.ethereum.on('accountsChanged', handleAccountsChanged)
window.ethereum.on('chainChanged', handleChainChanged)
await checkNetwork()
} catch (err) {
console.error('Ошибка при подключении кошелька:', err)
error.value = 'Ошибка при подключении кошелька: ' + err.message
}
}
// Обработчики событий MetaMask
function handleAccountsChanged(accounts) {
if (accounts.length === 0) {
// MetaMask отключен
isConnected.value = false
address.value = null
} else {
// Аккаунт изменен
address.value = accounts[0]
}
}
function handleChainChanged() {
// При смене сети перезагружаем страницу
window.location.reload()
}
// Обновляем watch
watch(isConnected, async (newValue) => {
try {
if (newValue) {
console.log('Кошелек подключен, адрес:', address.value)
if (!isInitialized.value) {
await initializeContract()
}
await checkNetwork()
} else {
console.log('Кошелек отключен')
currentPrice.value = null
contractOwner.value = null
isCorrectNetwork.value = false
error.value = ''
isInitialized.value = false
}
} catch (err) {
console.error('Ошибка при обработке подключения:', err)
error.value = 'Ошибка при обработке подключения: ' + err.message
}
}, { immediate: true })
// Обновляем функцию проверки сети
async function checkNetwork() {
try {
if (!walletProvider.value) {
isCorrectNetwork.value = false
error.value = 'Провайдер кошелька недоступен'
return
}
const ethersProvider = new BrowserProvider(walletProvider.value)
const network = await ethersProvider.getNetwork()
console.log('Текущая сеть:', network.chainId)
isCorrectNetwork.value = Number(network.chainId) === SEPOLIA_CHAIN_ID
if (!isCorrectNetwork.value) {
error.value = `Пожалуйста, переключитесь на сеть Sepolia (${SEPOLIA_CHAIN_ID}). Текущая сеть: ${network.chainId}`
} else {
error.value = ''
await Promise.all([fetchPrice(), fetchOwner()])
}
} catch (err) {
console.error('Ошибка при проверке сети:', err)
isCorrectNetwork.value = false
error.value = 'Ошибка при проверке сети: ' + err.message
}
}
// Обновляем функцию fetchPrice с обработкой ошибок
async function fetchPrice() {
try {
const contract = new Contract(contractAddress, contractABI, provider)
const price = await contract.price()
currentPrice.value = price
console.log('Текущая цена:', formatEther(price), 'ETH')
} catch (err) {
console.error('Ошибка при получении цены:', err)
error.value = `Не удалось получить текущую цену: ${err.message}`
currentPrice.value = null
}
}
// Получение адреса владельца
async function fetchOwner() {
try {
const contract = new Contract(contractAddress, contractABI, provider)
contractOwner.value = await contract.owner()
} catch (err) {
console.error('Ошибка при получении адреса владельца:', err)
}
}
// Обновляем onMounted
onMounted(async () => {
console.log('Компонент смонтирован')
try {
await initializeContract()
if (provider) {
const contract = new Contract(contractAddress, contractABI, provider)
contract.on('Purchase', (buyer, amount) => {
console.log(`Новая покупка: ${amount} единиц от ${buyer}`)
fetchPrice()
})
return () => {
contract.removeAllListeners('Purchase')
}
}
} catch (err) {
console.error('Ошибка при монтировании компонента:', err)
}
})
// Добавляем функцию для расчета общей стоимости
function calculateTotalCost() {
if (!currentPrice.value || !amount.value) return BigInt(0)
return currentPrice.value * BigInt(amount.value)
}
// Обновляем handlePurchase
async function handlePurchase() {
if (!amount.value) return
if (!isCorrectNetwork.value) {
error.value = 'Пожалуйста, переключитесь на сеть Sepolia'
return
}
error.value = ''
success.value = ''
try {
isLoading.value = true
const ethersProvider = new BrowserProvider(walletProvider.value)
const signer = await ethersProvider.getSigner()
const contract = new Contract(contractAddress, contractABI, signer)
const totalCost = calculateTotalCost()
// Проверяем баланс
const balance = await ethersProvider.getBalance(await signer.getAddress())
console.log('Баланс кошелька:', formatEther(balance), 'ETH')
if (balance < totalCost) {
throw new Error(`Недостаточно средств. Нужно ${formatEther(totalCost)} ETH`)
}
console.log('Общая стоимость:', formatEther(totalCost), 'ETH')
console.log('Параметры транзакции:', {
amount: amount.value,
totalCost: formatEther(totalCost),
from: await signer.getAddress()
})
const tx = await contract.purchase(amount.value, {
value: totalCost,
gasLimit: 100000 // Явно указываем лимит газа
})
console.log('Транзакция отправлена:', tx.hash)
await tx.wait()
console.log('Транзакция подтверждена')
amount.value = ''
success.value = 'Покупка успешно совершена!'
await fetchPrice()
} catch (err) {
console.error('Ошибка при покупке:', err)
console.error('Детали ошибки:', {
code: err.code,
message: err.message,
data: err.data,
reason: err.reason
})
if (err.message.includes('user rejected')) {
error.value = 'Транзакция отменена пользователем'
} else if (err.message.includes('Недостаточно средств')) {
error.value = err.message
} else {
error.value = 'Произошла ошибка при совершении покупки: ' + err.message
}
} finally {
isLoading.value = false
}
}
// Добавляем новые функции
async function handleSetPrice() {
if (!newPrice.value) return
error.value = ''
success.value = ''
try {
isLoading.value = true
const ethersProvider = new BrowserProvider(walletProvider.value)
const signer = await ethersProvider.getSigner()
const contract = new Contract(contractAddress, contractABI, signer)
const priceInWei = parseEther(newPrice.value.toString())
const tx = await contract.setPrice(priceInWei)
await tx.wait()
newPrice.value = ''
success.value = 'Цена успешно обновлена!'
await fetchPrice()
} catch (err) {
console.error('Ошибка при установке цены:', err)
error.value = 'Ошибка при установке цены: ' + err.message
} finally {
isLoading.value = false
}
}
async function handleWithdraw() {
error.value = ''
success.value = ''
try {
isLoading.value = true
const ethersProvider = new BrowserProvider(walletProvider.value)
const signer = await ethersProvider.getSigner()
const contract = new Contract(contractAddress, contractABI, signer)
const tx = await contract.withdraw()
await tx.wait()
success.value = 'Средства успешно выведены!'
} catch (err) {
console.error('Ошибка при выводе средств:', err)
error.value = 'Ошибка при выводе средств: ' + err.message
} finally {
isLoading.value = false
}
}
defineExpose({
isConnected,
address
})
</script>
<style scoped>
.contract-interaction {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.wallet-info {
margin-top: 20px;
padding: 15px;
background-color: #f5f5f5;
border-radius: 8px;
}
.contract-controls {
margin-top: 20px;
}
.input-group {
display: flex;
gap: 10px;
margin-top: 10px;
}
.amount-input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.purchase-button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.purchase-button:hover:not(:disabled) {
background-color: #45a049;
}
.purchase-button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.error-message {
color: #dc3545;
padding: 10px;
margin: 10px 0;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
.price-info {
margin: 10px 0;
font-weight: bold;
color: #2c3e50;
}
.owner-controls {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.purchase-panel {
margin-top: 20px;
}
.admin-button {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.admin-button:hover:not(:disabled) {
background-color: #0056b3;
}
.withdraw-button {
margin-top: 10px;
background-color: #6c757d;
}
.withdraw-button:hover:not(:disabled) {
background-color: #5a6268;
}
.total-cost {
margin-top: 10px;
font-size: 0.9em;
color: #6c757d;
}
.success-message {
color: #28a745;
margin-top: 10px;
font-size: 0.9em;
}
.loading-message {
color: #6c757d;
text-align: center;
padding: 20px;
background-color: #f8f9fa;
border-radius: 4px;
margin: 10px 0;
}
.auth-status {
margin: 10px 0;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.authenticated {
color: #28a745;
}
.signout-button {
padding: 5px 10px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.signout-button:hover {
background-color: #c82333;
}
.connect-button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.connect-button:hover {
background-color: #45a049;
}
</style>

View File

@@ -1,163 +0,0 @@
<template>
<div class="data-tables" v-if="isConnected">
<h3>Данные из базы</h3>
<!-- История чатов -->
<div class="table-section">
<h4>История чатов</h4>
<table>
<thead>
<tr>
<th>Адрес</th>
<th>Сообщение</th>
<th>Ответ</th>
<th>Дата</th>
</tr>
</thead>
<tbody>
<tr v-for="chat in chatHistory" :key="chat.id">
<td>{{ shortenAddress(chat.address) }}</td>
<td>{{ chat.message }}</td>
<td>{{ chat.response }}</td>
<td>{{ formatDate(chat.created_at) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Пользователи -->
<div class="table-section">
<h4>Пользователи</h4>
<table>
<thead>
<tr>
<th>ID</th>
<th>Адрес</th>
<th>Дата регистрации</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ shortenAddress(user.address) }}</td>
<td>{{ formatDate(user.created_at) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
const props = defineProps({
isConnected: Boolean,
userAddress: String
});
const chatHistory = ref([]);
const users = ref([]);
// Нормализация адреса (приведение к нижнему регистру)
function normalizeAddress(address) {
return address?.toLowerCase() || '';
}
// Следим за изменением состояния подключения
watch(() => props.isConnected, (newValue) => {
console.log('isConnected изменился:', newValue);
if (newValue) {
fetchData();
}
});
// Следим за изменением адреса
watch(() => props.userAddress, (newValue) => {
console.log('userAddress изменился:', newValue);
if (props.isConnected && newValue) {
fetchData();
}
});
// Получение данных
async function fetchData() {
try {
console.log('Запрос обновления данных');
// История чатов
const chatResponse = await fetch('http://127.0.0.1:3000/api/chat/history', {
credentials: 'include'
});
const chatData = await chatResponse.json();
console.log('Получена история чата:', chatData);
chatHistory.value = chatData.history.map(chat => ({
...chat,
address: normalizeAddress(chat.address)
}));
// Пользователи
const usersResponse = await fetch('http://127.0.0.1:3000/api/users', {
credentials: 'include'
});
const usersData = await usersResponse.json();
console.log('Получен список пользователей:', usersData);
users.value = usersData.users.map(user => ({
...user,
address: normalizeAddress(user.address)
}));
} catch (error) {
console.error('Ошибка получения данных:', error);
}
}
// Форматирование адреса
function shortenAddress(address) {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
// Форматирование даты
function formatDate(date) {
return new Date(date).toLocaleString();
}
onMounted(() => {
if (props.isConnected) {
fetchData();
}
});
// Делаем метод доступным извне
defineExpose({
fetchData
});
</script>
<style scoped>
.data-tables {
margin: 20px;
}
.table-section {
margin-bottom: 30px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f5f5f5;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
</style>

View File

@@ -1,54 +0,0 @@
<template>
<div class="server-control">
<button
@click="stopServer"
class="stop-button"
:disabled="isLoading"
>
{{ isLoading ? 'Останавливается...' : 'Остановить сервер' }}
</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isLoading = ref(false)
async function stopServer() {
if (!confirm('Вы уверены, что хотите остановить сервер?')) return
isLoading.value = true
try {
const response = await fetch('http://localhost:3000/shutdown', {
method: 'POST'
})
const data = await response.json()
console.log(data.message)
} catch (error) {
console.error('Ошибка при остановке сервера:', error)
} finally {
isLoading.value = false
}
}
</script>
<style scoped>
.server-control {
margin-top: 20px;
}
.stop-button {
background-color: #dc3545;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.stop-button:disabled {
background-color: #dc354580;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<div class="sidebar" :class="{ 'collapsed': isSidebarCollapsed }">
<div class="sidebar-content">
<!-- AI Assistant Section -->
<div class="sidebar-section">
<div class="section-header" @click="toggleSidebarOrSection">
<span v-if="!isSidebarCollapsed">
<span>ИИ Ассистент</span>
</span>
<span v-else class="section-letter">AI</span>
</div>
<div class="section-content" v-show="!isSidebarCollapsed">
<SidebarItem
to="/ai/chats"
text="Чаты"
/>
<template v-if="isAdmin">
<SidebarItem
to="/ai/users"
text="Пользователи"
/>
<SidebarItem
to="/ai/vectorstore"
text="Векторное хранилище"
/>
</template>
</div>
</div>
<!-- Smart Contract Section -->
<div v-if="isAdmin" class="sidebar-section">
<div class="section-header" @click="toggleSidebarOrSection">
<span v-if="!isSidebarCollapsed">
<span>Смарт Контракт</span>
</span>
<span v-else class="section-letter">SK</span>
</div>
<div class="section-content" v-show="!isSidebarCollapsed">
<SidebarItem
to="/contract/deploy"
icon="🚀"
text="Деплой"
/>
<SidebarItem
to="/contract/manage"
icon="⚙️"
text="Управление"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import SidebarItem from './SidebarItem.vue'
const props = defineProps({
isAdmin: {
type: Boolean,
required: true
}
})
const emit = defineEmits(['update:collapsed'])
const aiExpanded = ref(true)
const contractExpanded = ref(true)
const isSidebarCollapsed = ref(false)
function toggleAI() {
aiExpanded.value = !aiExpanded.value
}
function toggleContract() {
contractExpanded.value = !contractExpanded.value
}
function toggleSidebar() {
isSidebarCollapsed.value = !isSidebarCollapsed.value
emit('update:collapsed', isSidebarCollapsed.value)
}
function toggleSidebarOrSection(event) {
if (isSidebarCollapsed.value) {
isSidebarCollapsed.value = false
emit('update:collapsed', false)
}
}
</script>
<style scoped>
.sidebar {
width: 250px;
background: white;
color: #2c3e50;
padding: 1.5rem 0;
position: fixed;
top: 70px;
left: 0;
overflow-y: auto;
z-index: 100;
transition: all 0.3s ease;
height: calc(100vh - 70px);
}
.sidebar.collapsed {
width: 60px;
padding: 1rem 0;
background: #2c3e50;
}
.sidebar-section {
margin-bottom: 1rem;
color: #2c3e50;
}
.section-header {
display: flex;
align-items: center;
padding: 0.75rem 2rem;
cursor: pointer;
transition: background-color 0.3s;
color: #2c3e50;
font-weight: 500;
height: 44px;
user-select: none;
}
.section-header:hover {
background-color: rgba(0,0,0,0.05);
}
.icon {
display: none;
}
.section-content {
margin-left: 2rem;
padding-top: 0.25rem;
}
/* Стили для активных ссылок */
:deep(.router-link-active) {
background-color: rgba(0,0,0,0.1);
}
.section-letter {
color: white;
font-weight: bold;
font-size: 1.2rem;
cursor: pointer;
display: block;
text-align: center;
line-height: 40px;
height: 40px;
}
.collapsed .section-header {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.collapsed .section-header:hover .section-letter {
opacity: 0.8;
}
/* Стили для пунктов меню */
:deep(.sidebar-link) {
height: 36px;
display: flex;
align-items: center;
padding: 0 0.75rem;
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<router-link
:to="to"
class="sidebar-item"
:class="{ 'router-link-active': $route.path === to }"
custom
v-slot="{ navigate, isActive }"
>
<div
@click="navigate"
class="sidebar-link"
:class="{ active: isActive }"
>
<span class="text">{{ text }}</span>
</div>
</router-link>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router/dist/vue-router.esm-bundler'
const props = defineProps({
to: {
type: String,
required: true
},
icon: {
type: String,
default: '📄'
},
text: {
type: String,
required: true
}
})
const route = useRoute()
const isActive = computed(() => route.path === props.to)
</script>
<style scoped>
.sidebar-link {
display: flex;
align-items: center;
padding: 0 0.75rem;
color: #2c3e50;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s;
cursor: pointer;
height: 36px;
}
.sidebar-link:hover {
background-color: rgba(0,0,0,0.05);
}
.sidebar-link.active {
background-color: rgba(0,0,0,0.1);
font-weight: 500;
}
.icon {
margin-right: 0.5rem;
}
.text {
font-size: 0.9rem;
color: inherit;
line-height: 1;
}
</style>

View File

@@ -0,0 +1,243 @@
<template>
<div class="wallet-connection">
<div v-if="!isConnected" class="header">
<h1>DApp for Business</h1>
<button
@click="connectWallet"
class="connect-button"
>
Подключить кошелек
</button>
</div>
<div v-else class="header">
<h1>DApp for Business</h1>
<div class="wallet-info">
<span class="address">{{ shortenAddress(userAddress) }}</span>
<button @click="disconnectWallet" class="disconnect-btn">
Отключить
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({
isConnected: Boolean,
userAddress: String
});
const emit = defineEmits(['connect', 'disconnect']);
// Проверка сессии при загрузке
async function checkSession() {
try {
const response = await fetch('http://127.0.0.1:3000/api/session', {
credentials: 'include'
});
if (!response.ok) return;
const data = await response.json();
if (data.authenticated) {
emit('connect', data.address);
}
} catch (error) {
console.error('Ошибка проверки сессии:', error);
}
}
async function connectWallet() {
try {
if (!window.ethereum) {
alert('Пожалуйста, установите MetaMask для работы с приложением');
window.open('https://metamask.io/download.html', '_blank');
return;
}
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
if (accounts.length > 0) {
const address = accounts[0];
// Получаем nonce
const nonceResponse = await fetch('http://127.0.0.1:3000/api/nonce', {
credentials: 'include'
});
if (!nonceResponse.ok) {
throw new Error('Failed to get nonce');
}
const { nonce } = await nonceResponse.json();
// Создаем сообщение для подписи
const message = {
domain: window.location.host,
address: address,
statement: 'Sign in with Ethereum to access DApp features and AI Assistant',
uri: window.location.origin,
version: '1',
chainId: '11155111',
nonce: nonce,
issuedAt: new Date().toISOString(),
resources: [
`${window.location.origin}/api/chat`,
`${window.location.origin}/api/contract`
]
};
console.log('Подписываем сообщение:', message);
// Получаем подпись
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [
JSON.stringify(message, null, 2),
address
]
});
console.log('Получена подпись:', signature);
// Верифицируем подпись
const verifyResponse = await fetch('http://127.0.0.1:3000/api/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
message,
signature
})
});
if (!verifyResponse.ok) {
const error = await verifyResponse.text();
throw new Error(`Failed to verify signature: ${error}`);
}
const { isAdmin } = await verifyResponse.json();
emit('connect', address);
return isAdmin;
}
} catch (error) {
console.error('Ошибка подключения:', error);
alert('Ошибка подключения кошелька: ' + error.message);
}
}
async function disconnectWallet() {
try {
// Выходим из системы
const response = await fetch('http://127.0.0.1:3000/api/signout', {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to sign out');
}
emit('disconnect');
} catch (error) {
console.error('Error disconnecting:', error);
alert('Ошибка при отключении');
}
}
function shortenAddress(address) {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
onMounted(() => {
checkSession();
});
</script>
<style scoped>
.wallet-connection {
display: flex;
justify-content: space-between;
padding: 1.5rem 2rem;
background: white;
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 99;
box-sizing: border-box;
}
.header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
margin: 0;
font-size: 1.5rem;
color: #2c3e50;
font-weight: 600;
line-height: 1;
}
.connect-button {
padding: 0.5rem 1rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
min-width: 180px;
text-align: center;
}
.connect-btn,
.disconnect-btn {
padding: 0.5rem 1rem;
height: 36px;
line-height: 1;
border: none;
border-radius: 4px;
cursor: pointer;
color: white;
}
.connect-btn {
background: #28a745;
}
.disconnect-btn {
padding: 0.5rem 1rem;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
}
.wallet-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.address {
font-family: monospace;
background: #e9ecef;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.9rem;
}
</style>