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

This commit is contained in:
2025-02-21 17:11:14 +03:00
parent 69104d9db3
commit 2ddd4a9ff0
13 changed files with 1331 additions and 53 deletions

View File

@@ -1,12 +1,32 @@
<template>
<div id="app">
<h1>DApp for Business</h1>
<ContractInteraction />
<ContractInteraction ref="contractInteraction" />
<AIAssistant
:isConnected="isConnected"
:userAddress="userAddress"
/>
<ServerControl />
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import ContractInteraction from './components/ContractInteraction.vue'
import AIAssistant from './components/AIAssistant.vue'
import ServerControl from './components/ServerControl.vue'
const contractInteraction = ref(null)
const isConnected = ref(false)
const userAddress = ref(null)
watch(() => contractInteraction.value?.isConnected, (newValue) => {
isConnected.value = newValue
})
watch(() => contractInteraction.value?.address, (newValue) => {
userAddress.value = newValue
})
</script>
<style>

View File

@@ -0,0 +1,237 @@
<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 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 = ''
} 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

@@ -85,7 +85,7 @@
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { BrowserProvider, Contract, JsonRpcProvider, formatEther, parseEther } from 'ethers'
import { BrowserProvider, Contract, JsonRpcProvider, formatEther, parseEther, getAddress } from 'ethers'
// Инициализируем все ref переменные в начале
const amount = ref('')
@@ -101,6 +101,7 @@ 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
@@ -209,6 +210,60 @@ async function connectWallet() {
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)
@@ -458,6 +513,11 @@ async function handleWithdraw() {
isLoading.value = false
}
}
defineExpose({
isConnected,
address
})
</script>
<style scoped>

View File

@@ -0,0 +1,54 @@
<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>