Описание изменений
This commit is contained in:
5
backend/.gitignore
vendored
5
backend/.gitignore
vendored
@@ -26,4 +26,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
coverage/
|
coverage/
|
||||||
coverage.json
|
coverage.json
|
||||||
|
|
||||||
|
# Sessions directory
|
||||||
|
sessions/
|
||||||
@@ -5,6 +5,11 @@ import "@openzeppelin/contracts/access/Ownable.sol";
|
|||||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||||
|
|
||||||
contract MyContract is Ownable, ReentrancyGuard {
|
contract MyContract is Ownable, ReentrancyGuard {
|
||||||
|
// Явно объявляем функцию owner
|
||||||
|
function owner() public view override returns (address) {
|
||||||
|
return super.owner();
|
||||||
|
}
|
||||||
|
|
||||||
uint256 public price;
|
uint256 public price;
|
||||||
|
|
||||||
event Purchase(address buyer, uint256 amount);
|
event Purchase(address buyer, uint256 amount);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
DROP TABLE IF EXISTS documents;
|
DROP TABLE IF EXISTS documents;
|
||||||
CREATE TABLE documents (
|
CREATE TABLE documents (
|
||||||
id bigserial PRIMARY KEY,
|
id bigserial PRIMARY KEY,
|
||||||
content text,
|
content text NOT NULL,
|
||||||
metadata jsonb,
|
metadata jsonb,
|
||||||
embedding vector(4096)
|
embedding vector(4096)
|
||||||
);
|
);
|
||||||
@@ -24,7 +24,8 @@ CREATE TABLE IF NOT EXISTS chat_history (
|
|||||||
message TEXT NOT NULL,
|
message TEXT NOT NULL,
|
||||||
response TEXT NOT NULL,
|
response TEXT NOT NULL,
|
||||||
context_docs INTEGER[],
|
context_docs INTEGER[],
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_approved BOOLEAN DEFAULT false
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Даем права пользователю
|
-- Даем права пользователю
|
||||||
@@ -37,4 +38,7 @@ UPDATE users SET address = LOWER(address);
|
|||||||
-- Удаляем дубликаты
|
-- Удаляем дубликаты
|
||||||
DELETE FROM users a USING users b
|
DELETE FROM users a USING users b
|
||||||
WHERE a.id > b.id
|
WHERE a.id > b.id
|
||||||
AND LOWER(a.address) = LOWER(b.address);
|
AND LOWER(a.address) = LOWER(b.address);
|
||||||
|
|
||||||
|
ALTER TABLE chat_history
|
||||||
|
ADD COLUMN IF NOT EXISTS is_approved BOOLEAN DEFAULT false;
|
||||||
@@ -7,6 +7,9 @@ const { RunnableSequence } = require('@langchain/core/runnables');
|
|||||||
const { StringOutputParser } = require('@langchain/core/output_parsers');
|
const { StringOutputParser } = require('@langchain/core/output_parsers');
|
||||||
const { PromptTemplate } = require('@langchain/core/prompts');
|
const { PromptTemplate } = require('@langchain/core/prompts');
|
||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const contractABI = require('../artifacts/contracts/MyContract.sol/MyContract.json').abi;
|
||||||
|
const crypto = require('crypto');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
@@ -16,30 +19,117 @@ const pool = new Pool({
|
|||||||
|
|
||||||
const chat = new ChatOllama({
|
const chat = new ChatOllama({
|
||||||
model: 'mistral',
|
model: 'mistral',
|
||||||
baseUrl: 'http://localhost:11434'
|
baseUrl: 'http://localhost:11434',
|
||||||
|
temperature: 0.7,
|
||||||
|
format: 'json'
|
||||||
});
|
});
|
||||||
|
|
||||||
const embeddings = new OllamaEmbeddings({
|
const embeddings = new OllamaEmbeddings({
|
||||||
model: 'mistral',
|
model: 'mistral',
|
||||||
baseUrl: 'http://localhost:11434'
|
baseUrl: 'http://localhost:11434',
|
||||||
|
requestOptions: {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dimensions: 4096,
|
||||||
|
stripNewLines: true,
|
||||||
|
maxConcurrency: 1,
|
||||||
|
maxRetries: 3,
|
||||||
|
timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
let vectorStore;
|
let vectorStore;
|
||||||
|
let contract;
|
||||||
|
|
||||||
async function initVectorStore() {
|
async function initVectorStore() {
|
||||||
vectorStore = await PGVectorStore.initialize(
|
try {
|
||||||
embeddings,
|
console.log('Начинаем инициализацию векторного хранилища...');
|
||||||
{
|
vectorStore = await PGVectorStore.initialize(
|
||||||
postgresConnectionOptions: {
|
embeddings,
|
||||||
connectionString: process.env.DATABASE_URL
|
{
|
||||||
|
postgresConnectionOptions: {
|
||||||
|
connectionString: process.env.DATABASE_URL
|
||||||
|
},
|
||||||
|
tableName: 'documents',
|
||||||
|
columns: {
|
||||||
|
idColumnName: 'id',
|
||||||
|
vectorColumnName: 'embedding',
|
||||||
|
contentColumnName: 'content',
|
||||||
|
metadataColumnName: 'metadata',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('Векторное хранилище инициализировано:', {
|
||||||
|
tableName: 'documents',
|
||||||
|
columns: {
|
||||||
|
structure: (await pool.query(`
|
||||||
|
SELECT
|
||||||
|
column_name,
|
||||||
|
data_type,
|
||||||
|
is_nullable,
|
||||||
|
column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'documents'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
`)).rows.map(row => ({
|
||||||
|
name: row.column_name,
|
||||||
|
type: row.data_type,
|
||||||
|
nullable: row.is_nullable,
|
||||||
|
default: row.column_default
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
tableName: 'documents'
|
config: {
|
||||||
|
tableName: vectorStore.tableName,
|
||||||
|
columns: vectorStore.columns,
|
||||||
|
client: vectorStore.client ? 'Connected' : 'Not connected',
|
||||||
|
embeddings: vectorStore.embeddings ? 'Initialized' : 'Not initialized'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка инициализации векторного хранилища:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initContract() {
|
||||||
|
try {
|
||||||
|
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
|
||||||
|
// Проверяем подключение к сети
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
console.log('Подключены к сети:', network.chainId);
|
||||||
|
|
||||||
|
contract = new ethers.Contract(
|
||||||
|
process.env.CONTRACT_ADDRESS,
|
||||||
|
contractABI,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
|
// Проверяем что контракт существует
|
||||||
|
const code = await provider.getCode(process.env.CONTRACT_ADDRESS);
|
||||||
|
if (code === '0x') {
|
||||||
|
throw new Error('Contract not deployed at this address');
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
// Проверяем подключение
|
||||||
|
const owner = await contract.owner();
|
||||||
|
console.log('Владелец контракта:', owner);
|
||||||
|
console.log('Контракт инициализирован:', process.env.CONTRACT_ADDRESS);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка инициализации контракта:', error);
|
||||||
|
// Если контракт не найден, не пытаемся переподключиться
|
||||||
|
if (error.message.includes('not deployed')) {
|
||||||
|
console.error('Контракт не найден по указанному адресу');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Пробуем переподключиться через 5 секунд
|
||||||
|
setTimeout(initContract, 5000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализируем при старте
|
// Инициализируем при старте
|
||||||
initVectorStore().catch(console.error);
|
initVectorStore().catch(console.error);
|
||||||
|
initContract().catch(console.error);
|
||||||
|
|
||||||
// Проверяем подключение к БД при старте
|
// Проверяем подключение к БД при старте
|
||||||
pool.connect((err, client, release) => {
|
pool.connect((err, client, release) => {
|
||||||
@@ -59,16 +149,38 @@ function requireAuth(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Генерация случайного nonce
|
||||||
|
function generateNonce() {
|
||||||
|
return crypto.randomBytes(16).toString('base64').replace(/[^a-zA-Z0-9]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение nonce для подписи
|
||||||
|
router.get('/nonce', (req, res) => {
|
||||||
|
try {
|
||||||
|
setCorsHeaders(res);
|
||||||
|
const nonce = generateNonce();
|
||||||
|
console.log('Сгенерирован новый nonce:', nonce);
|
||||||
|
res.json({ nonce });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка генерации nonce:', error);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Верификация подписи
|
||||||
router.post('/verify', async (req, res) => {
|
router.post('/verify', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { message, signature } = req.body;
|
const { message, signature } = req.body;
|
||||||
|
|
||||||
// ... верификация подписи ...
|
// Обновляем данные сессии
|
||||||
|
Object.assign(req.session, {
|
||||||
// Сохраняем в сессию
|
authenticated: true,
|
||||||
req.session.authenticated = true;
|
siwe: message,
|
||||||
req.session.siwe = message;
|
userAddress: message.address,
|
||||||
req.session.userAddress = message.address;
|
cookie: {
|
||||||
|
maxAge: 7 * 24 * 60 * 60 * 1000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Ждем сохранения
|
// Ждем сохранения
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -81,7 +193,20 @@ router.post('/verify', async (req, res) => {
|
|||||||
address: req.session.userAddress
|
address: req.session.userAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ ok: true });
|
// Проверяем права админа сразу после входа
|
||||||
|
const contractOwner = await contract.owner();
|
||||||
|
const isAdmin = message.address.toLowerCase() === contractOwner.toLowerCase();
|
||||||
|
|
||||||
|
console.log('Проверка прав после входа:', {
|
||||||
|
userAddress: message.address,
|
||||||
|
contractOwner,
|
||||||
|
isAdmin
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
ok: true,
|
||||||
|
isAdmin
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Verify error:', error);
|
console.error('Verify error:', error);
|
||||||
res.status(400).json({ error: error.message });
|
res.status(400).json({ error: error.message });
|
||||||
@@ -89,13 +214,13 @@ router.post('/verify', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Создаем шаблон промпта для RAG
|
// Создаем шаблон промпта для RAG
|
||||||
const TEMPLATE = `Вы - ассистент в DApp приложении.
|
const TEMPLATE = `Вы - ассистент в DApp приложении. Используйте следующий контекст для ответа:
|
||||||
Используйте этот контекст для ответа на вопрос:
|
|
||||||
{context}
|
|
||||||
|
|
||||||
Вопрос: {question}
|
Контекст: {context}
|
||||||
|
Вопрос пользователя: {question}
|
||||||
|
|
||||||
Ответ должен быть полезным, точным и основанным на предоставленном контексте.`;
|
Отвечайте кратко и по существу, основываясь на предоставленном контексте. Если контекст пустой или не релевантный,
|
||||||
|
используйте свои базовые знания о DApp и блокчейне.`;
|
||||||
|
|
||||||
const prompt = PromptTemplate.fromTemplate(TEMPLATE);
|
const prompt = PromptTemplate.fromTemplate(TEMPLATE);
|
||||||
|
|
||||||
@@ -103,106 +228,145 @@ const prompt = PromptTemplate.fromTemplate(TEMPLATE);
|
|||||||
const chain = RunnableSequence.from([
|
const chain = RunnableSequence.from([
|
||||||
{
|
{
|
||||||
context: async (input) => {
|
context: async (input) => {
|
||||||
const results = await vectorStore.similaritySearch(input.question);
|
try {
|
||||||
return results.map(doc => doc.content).join('\n\n');
|
const results = await vectorStore.similaritySearch(
|
||||||
|
input.question,
|
||||||
|
1,
|
||||||
|
{ type: 'approved_chat' }
|
||||||
|
);
|
||||||
|
if (!results.length) return '';
|
||||||
|
return results
|
||||||
|
.filter(doc => doc.pageContent)
|
||||||
|
.map(doc => doc.pageContent)
|
||||||
|
.join('\n\n');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка поиска контекста:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
question: (input) => input.question
|
question: (input) => input.message
|
||||||
},
|
},
|
||||||
prompt,
|
prompt,
|
||||||
chat,
|
chat,
|
||||||
new StringOutputParser()
|
new StringOutputParser()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Функция проверки работоспособности эмбеддингов
|
||||||
|
async function checkEmbeddings() {
|
||||||
|
try {
|
||||||
|
const testEmbed = await embeddings.embedQuery('test');
|
||||||
|
console.log('Эмбеддинги работают, размерность:', testEmbed.length);
|
||||||
|
if (testEmbed.length !== 4096) {
|
||||||
|
throw new Error(`Неверная размерность: ${testEmbed.length}, ожидалось: 4096`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка эмбеддингов:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
router.post('/chat', requireAuth, async (req, res) => {
|
router.post('/chat', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (req.session.siwe.address.toLowerCase() !== req.body.userAddress.toLowerCase()) {
|
const { message } = req.body;
|
||||||
return res.status(401).json({
|
const userAddress = req.session.siwe.address;
|
||||||
error: 'Address mismatch'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!vectorStore) {
|
|
||||||
throw new Error('Vector store not initialized');
|
|
||||||
}
|
|
||||||
const { message, userAddress } = req.body;
|
|
||||||
console.log('Session in chat:', req.session);
|
|
||||||
console.log('User address:', userAddress);
|
|
||||||
|
|
||||||
// Получаем или создаем пользователя
|
// Получаем или создаем пользователя
|
||||||
let user = await pool.query(
|
let userResult = await pool.query(
|
||||||
'INSERT INTO users (address) VALUES (LOWER($1)) ON CONFLICT (address) DO UPDATE SET address = LOWER($1) RETURNING id',
|
'SELECT id FROM users WHERE LOWER(address) = LOWER($1)',
|
||||||
[userAddress]
|
[userAddress]
|
||||||
);
|
);
|
||||||
const userId = user.rows[0].id;
|
|
||||||
|
if (userResult.rows.length === 0) {
|
||||||
|
userResult = await pool.query(
|
||||||
|
'INSERT INTO users (address) VALUES (LOWER($1)) RETURNING id',
|
||||||
|
[userAddress]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = userResult.rows[0].id;
|
||||||
|
|
||||||
// Получаем релевантные документы для контекста
|
// Создаем входные данные для chain
|
||||||
const results = await vectorStore.similaritySearch(message);
|
const input = {
|
||||||
console.log('Found documents:', results);
|
message: message,
|
||||||
const contextIds = results.map(doc => doc.id);
|
|
||||||
|
|
||||||
const response = await chain.invoke({
|
|
||||||
question: message
|
question: message
|
||||||
});
|
};
|
||||||
|
|
||||||
// Сохраняем историю
|
// Проверяем эмбеддинги перед использованием
|
||||||
await pool.query(
|
if (!await checkEmbeddings()) {
|
||||||
'INSERT INTO chat_history (user_id, message, response, context_docs) VALUES ($1, $2, $3, $4)',
|
console.warn('Embeddings service unavailable, continuing without context');
|
||||||
[userId, message, response, contextIds]
|
try {
|
||||||
);
|
const response = await chain.invoke(input);
|
||||||
|
|
||||||
|
// Сохраняем в базу без контекста
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO chat_history (user_id, message, response) VALUES ($1, $2, $3)',
|
||||||
|
[userId, message, response]
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({ response });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка генерации ответа:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ response: response });
|
const response = await chain.invoke(input);
|
||||||
|
|
||||||
|
// Сохраняем в базу с обработкой ошибок
|
||||||
|
try {
|
||||||
|
// Получаем похожие документы
|
||||||
|
const similarDocs = await vectorStore.similaritySearch(
|
||||||
|
message,
|
||||||
|
1,
|
||||||
|
{ type: 'approved_chat' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Извлекаем ID чатов из метаданных
|
||||||
|
const contextIds = similarDocs
|
||||||
|
.map(doc => doc.metadata?.chatId)
|
||||||
|
.filter(id => typeof id === 'number');
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO chat_history (user_id, message, response, context_docs) VALUES ($1, $2, $3, $4::integer[])',
|
||||||
|
[userId, message, response, contextIds]
|
||||||
|
);
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error('Ошибка сохранения в БД:', dbError);
|
||||||
|
// Продолжаем выполнение даже при ошибке сохранения
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ response });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка чата:', error);
|
console.error('Ошибка чата:', error);
|
||||||
if (error.code === 'unsupported_country_region_territory') {
|
res.status(500).json({
|
||||||
return res.status(503).json({
|
error: error.message,
|
||||||
error: 'Сервис временно недоступен в вашем регионе'
|
details: error.stack
|
||||||
});
|
});
|
||||||
}
|
|
||||||
res.status(500).json({ error: 'Ошибка сервера' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получение истории чата
|
// Получение истории чата
|
||||||
router.get('/chat/history', requireAuth, async (req, res) => {
|
router.get('/chat/history', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userAddress = req.session.siwe.address;
|
setCorsHeaders(res);
|
||||||
console.log('Запрос истории чата для:', userAddress);
|
|
||||||
|
|
||||||
// Получаем ID пользователя
|
const userAddress = req.session.siwe.address;
|
||||||
const userResult = await pool.query(
|
|
||||||
'SELECT id FROM users WHERE LOWER(address) = LOWER($1) ORDER BY created_at ASC LIMIT 1',
|
// Получаем историю чата пользователя
|
||||||
[userAddress]
|
const result = await pool.query(
|
||||||
);
|
`SELECT ch.*
|
||||||
console.log('Найден пользователь:', userResult.rows);
|
|
||||||
|
|
||||||
if (userResult.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'User not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = userResult.rows[0].id;
|
|
||||||
|
|
||||||
// Получаем историю чата
|
|
||||||
const history = await pool.query(
|
|
||||||
`SELECT
|
|
||||||
ch.id,
|
|
||||||
LOWER(u.address) as address,
|
|
||||||
ch.message,
|
|
||||||
ch.response,
|
|
||||||
ch.created_at
|
|
||||||
FROM chat_history ch
|
FROM chat_history ch
|
||||||
JOIN users u ON ch.user_id = u.id
|
JOIN users u ON ch.user_id = u.id
|
||||||
WHERE ch.user_id = $1
|
WHERE LOWER(u.address) = LOWER($1)
|
||||||
ORDER BY created_at DESC`,
|
ORDER BY ch.created_at DESC`,
|
||||||
[userId]
|
[userAddress]
|
||||||
);
|
);
|
||||||
console.log('История чата:', history.rows);
|
|
||||||
|
res.json({ history: result.rows });
|
||||||
res.json({
|
|
||||||
history: history.rows
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка получения истории:', error);
|
console.error('Ошибка получения истории:', error);
|
||||||
res.status(500).json({ error: 'Ошибка сервера' });
|
res.status(500).json({ error: 'Server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,4 +385,433 @@ router.get('/users', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Проверка на админа
|
||||||
|
router.get('/admin/check', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!contract) {
|
||||||
|
await initContract();
|
||||||
|
if (!contract) {
|
||||||
|
throw new Error('Contract not initialized');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем адрес из сессии
|
||||||
|
const userAddress = req.session.siwe.address;
|
||||||
|
console.log('Проверка админа, адрес из сессии:', userAddress);
|
||||||
|
|
||||||
|
const contractOwner = await contract.owner();
|
||||||
|
console.log('Проверка админа:', {
|
||||||
|
userAddress,
|
||||||
|
contractOwner
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAdmin = userAddress.toLowerCase() === contractOwner.toLowerCase();
|
||||||
|
console.log('Результат проверки админа:', isAdmin);
|
||||||
|
|
||||||
|
res.json({ isAdmin });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки админа:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Server error',
|
||||||
|
details: error.message,
|
||||||
|
code: error.code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Общая функция для установки CORS заголовков
|
||||||
|
function setCorsHeaders(res) {
|
||||||
|
res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5173');
|
||||||
|
res.header('Access-Control-Allow-Credentials', 'true');
|
||||||
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Accept');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение всех чатов для админа
|
||||||
|
router.get('/admin/chats', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
setCorsHeaders(res);
|
||||||
|
|
||||||
|
const chats = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
ch.id,
|
||||||
|
LOWER(u.address) as address,
|
||||||
|
ch.message,
|
||||||
|
ch.response,
|
||||||
|
ch.created_at,
|
||||||
|
ch.context_docs,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM documents d
|
||||||
|
WHERE d.metadata->>'chatId' = ch.id::text
|
||||||
|
AND d.metadata->>'type' = 'approved_chat'
|
||||||
|
) as is_approved
|
||||||
|
FROM chat_history ch
|
||||||
|
JOIN users u ON ch.user_id = u.id
|
||||||
|
ORDER BY ch.created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('Получено чатов:', chats.rows.length);
|
||||||
|
if (chats.rows.length > 0) {
|
||||||
|
console.log('Пример чата:', {
|
||||||
|
id: chats.rows[0].id,
|
||||||
|
address: chats.rows[0].address,
|
||||||
|
is_approved: chats.rows[0].is_approved
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ chats: chats.rows });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения чатов:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Server error',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Одобрение чата для обучения
|
||||||
|
router.post('/admin/approve', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userAddress = req.session.siwe.address;
|
||||||
|
const contractOwner = await contract.owner();
|
||||||
|
|
||||||
|
if (userAddress.toLowerCase() !== contractOwner.toLowerCase()) {
|
||||||
|
return res.status(403).json({ error: 'Not authorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chatId } = req.body;
|
||||||
|
|
||||||
|
// Обновляем статус в базе
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE chat_history SET is_approved = true WHERE id = $1',
|
||||||
|
[chatId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Добавляем в векторное хранилище для обучения
|
||||||
|
const chat = await pool.query(
|
||||||
|
`SELECT message, response FROM chat_history WHERE id = $1`,
|
||||||
|
[chatId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (chat.rows.length > 0) {
|
||||||
|
const { message, response } = chat.rows[0];
|
||||||
|
console.log('Добавляем в векторное хранилище:', {
|
||||||
|
message: message.substring(0, 50) + '...',
|
||||||
|
response: response.substring(0, 50) + '...',
|
||||||
|
chatId
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = {
|
||||||
|
pageContent: `Q: ${message}\nA: ${response}`,
|
||||||
|
metadata: {
|
||||||
|
type: 'approved_chat',
|
||||||
|
approvedBy: userAddress,
|
||||||
|
chatId: chatId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Проверяем работу эмбеддингов
|
||||||
|
try {
|
||||||
|
const testEmbedding = await embeddings.embedQuery('test');
|
||||||
|
console.log('Эмбеддинги работают, размерность:', testEmbedding.length);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки эмбеддингов:', error);
|
||||||
|
throw new Error('Embeddings error: ' + error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Документ для добавления:', {
|
||||||
|
pageContent: document.pageContent.substring(0, 100) + '...',
|
||||||
|
metadata: document.metadata,
|
||||||
|
vectorStore: {
|
||||||
|
tableName: vectorStore.tableName,
|
||||||
|
columns: vectorStore.columns
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем существование таблицы и её структуру
|
||||||
|
const tableInfo = await pool.query(`
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_name = 'documents'
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('Таблица documents существует:', tableInfo.rows[0].exists);
|
||||||
|
|
||||||
|
if (tableInfo.rows[0].exists) {
|
||||||
|
const columns = await pool.query(`
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'documents'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
`);
|
||||||
|
console.log('Структура таблицы documents:',
|
||||||
|
columns.rows.map(row => `${row.column_name} (${row.data_type})`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await vectorStore.addDocuments([
|
||||||
|
document
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Проверяем, что документ добавлен
|
||||||
|
const added = await vectorStore.similaritySearch(
|
||||||
|
document.pageContent,
|
||||||
|
1,
|
||||||
|
{ chatId: chatId }
|
||||||
|
);
|
||||||
|
console.log('Проверка добавления документа:', {
|
||||||
|
found: added.length > 0,
|
||||||
|
document: added[0]?.pageContent.substring(0, 100) + '...'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Успешно добавлено в векторное хранилище');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка одобрения:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Server error',
|
||||||
|
details: error.message,
|
||||||
|
code: error.code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Улучшаем проверку авторизации админа
|
||||||
|
async function requireAdmin(req, res, next) {
|
||||||
|
if (!req.session?.siwe?.address) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Not authenticated',
|
||||||
|
details: 'Please sign in first'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем адреса
|
||||||
|
const userAddress = req.session.siwe.address;
|
||||||
|
const contractOwner = await contract.owner();
|
||||||
|
|
||||||
|
console.log('Проверка админа:', {
|
||||||
|
userAddress: userAddress,
|
||||||
|
contractOwner: contractOwner
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userAddress.toLowerCase() !== contractOwner.toLowerCase()) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'Not authorized',
|
||||||
|
details: 'Only contract owner can access this endpoint'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки админа:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Server error',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение векторного хранилища для админа
|
||||||
|
router.get('/admin/vectors', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
setCorsHeaders(res);
|
||||||
|
|
||||||
|
// Добавляем колонку created_at если её нет
|
||||||
|
await pool.query(`
|
||||||
|
ALTER TABLE documents
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
`);
|
||||||
|
console.log('Проверена/добавлена колонка created_at');
|
||||||
|
|
||||||
|
// Проверяем структуру таблицы
|
||||||
|
const tableInfo = await pool.query(`
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'documents'
|
||||||
|
`);
|
||||||
|
console.log('Структура таблицы documents:', tableInfo.rows);
|
||||||
|
|
||||||
|
// Получаем все документы из векторного хранилища
|
||||||
|
const documents = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
d.id,
|
||||||
|
d.content,
|
||||||
|
d.metadata,
|
||||||
|
length(d.embedding::text) as embedding_size,
|
||||||
|
COALESCE(d.created_at, CURRENT_TIMESTAMP) as created_at,
|
||||||
|
CASE
|
||||||
|
WHEN d.metadata->>'type' = 'approved_chat' THEN true
|
||||||
|
ELSE false
|
||||||
|
END as is_approved
|
||||||
|
FROM documents d
|
||||||
|
ORDER BY d.created_at DESC NULLS LAST
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Форматируем ответ
|
||||||
|
const vectors = documents.rows.map(doc => ({
|
||||||
|
id: doc.id,
|
||||||
|
content: doc.content,
|
||||||
|
metadata: doc.metadata,
|
||||||
|
embedding_size: doc.embedding ? 4096 : 0, // Фиксированный размер для mistral
|
||||||
|
created: doc.created_at,
|
||||||
|
is_approved: doc.is_approved
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('Получено векторов:', vectors.length);
|
||||||
|
console.log('Пример вектора:', vectors[0]);
|
||||||
|
res.json({ vectors });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения векторов:', error);
|
||||||
|
console.error('Детали ошибки:', {
|
||||||
|
code: error.code,
|
||||||
|
detail: error.detail,
|
||||||
|
hint: error.hint,
|
||||||
|
position: error.position
|
||||||
|
});
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Server error',
|
||||||
|
details: error.message,
|
||||||
|
code: error.code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка CORS preflight запросов для админских роутов
|
||||||
|
router.options('/admin/*', (req, res) => {
|
||||||
|
setCorsHeaders(res);
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Очистка кэша и данных
|
||||||
|
router.post('/admin/clear-cache', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
setCorsHeaders(res);
|
||||||
|
|
||||||
|
// Очищаем таблицы
|
||||||
|
await pool.query('TRUNCATE TABLE documents CASCADE');
|
||||||
|
await pool.query('TRUNCATE TABLE chat_history CASCADE');
|
||||||
|
await pool.query('TRUNCATE TABLE users CASCADE');
|
||||||
|
|
||||||
|
// Сбрасываем автоинкремент
|
||||||
|
await pool.query('ALTER SEQUENCE documents_id_seq RESTART WITH 1');
|
||||||
|
await pool.query('ALTER SEQUENCE chat_history_id_seq RESTART WITH 1');
|
||||||
|
await pool.query('ALTER SEQUENCE users_id_seq RESTART WITH 1');
|
||||||
|
|
||||||
|
// Реинициализируем векторное хранилище
|
||||||
|
await initVectorStore();
|
||||||
|
|
||||||
|
console.log('Кэш и данные очищены');
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка очистки кэша:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Server error',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Выход из системы
|
||||||
|
router.post('/signout', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
setCorsHeaders(res);
|
||||||
|
|
||||||
|
// Уничтожаем сессию
|
||||||
|
req.session.destroy((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка при удалении сессии:', err);
|
||||||
|
return res.status(500).json({ error: 'Failed to destroy session' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Сессия успешно завершена');
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка выхода:', error);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверка сессии
|
||||||
|
router.get('/session', (req, res) => {
|
||||||
|
try {
|
||||||
|
setCorsHeaders(res);
|
||||||
|
|
||||||
|
if (req.session?.authenticated && req.session?.siwe?.address) {
|
||||||
|
res.json({
|
||||||
|
authenticated: true,
|
||||||
|
address: req.session.siwe.address
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({
|
||||||
|
authenticated: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки сессии:', error);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создание нового пользователя
|
||||||
|
router.post('/users', async (req, res) => {
|
||||||
|
try {
|
||||||
|
setCorsHeaders(res);
|
||||||
|
|
||||||
|
const { address } = req.body;
|
||||||
|
|
||||||
|
// Проверяем существование пользователя
|
||||||
|
const existingUser = await pool.query(
|
||||||
|
'SELECT * FROM users WHERE address = $1',
|
||||||
|
[address.toLowerCase()]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingUser.rows.length > 0) {
|
||||||
|
return res.json({ user: existingUser.rows[0] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем нового пользователя
|
||||||
|
const result = await pool.query(
|
||||||
|
'INSERT INTO users (address) VALUES ($1) RETURNING *',
|
||||||
|
[address.toLowerCase()]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ user: result.rows[0] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка создания пользователя:', error);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создание необходимых таблиц при старте
|
||||||
|
async function initializeTables() {
|
||||||
|
try {
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
address VARCHAR(42) NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_history (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
message TEXT,
|
||||||
|
response TEXT,
|
||||||
|
is_user BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('Таблицы успешно инициализированы');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка инициализации таблиц:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем инициализацию при старте
|
||||||
|
initializeTables();
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -1 +1 @@
|
|||||||
{"cookie":{"originalMaxAge":2591999999,"expires":"2025-03-23T16:06:19.831Z","secure":false,"httpOnly":true,"domain":"127.0.0.1","path":"/","sameSite":"lax"},"nonce":null,"__lastAccess":1740153979832,"siwe":{"domain":"127.0.0.1:5173","address":"0xF45aa4917b3775bA37f48Aeb3dc1a943561e9e0B","statement":"Sign in with Ethereum to access DApp features and AI Assistant","uri":"http://127.0.0.1:5173","version":"1","nonce":"u7P3wT2kyPmGb4Z0I","issuedAt":"2025-02-21T16:05:49.561Z","chainId":11155111,"resources":["http://127.0.0.1:5173/api/chat","http://127.0.0.1:5173/api/contract"]},"authenticated":true}
|
{"cookie":{"originalMaxAge":2591999999,"expires":"2025-03-24T09:38:02.826Z","secure":false,"httpOnly":true,"domain":"127.0.0.1","path":"/","sameSite":"lax"},"nonce":null,"__lastAccess":1740217082827,"siwe":{"domain":"127.0.0.1:5173","address":"0xF45aa4917b3775bA37f48Aeb3dc1a943561e9e0B","statement":"Sign in with Ethereum to access DApp features and AI Assistant","uri":"http://127.0.0.1:5173","version":"1","nonce":"3ZIxF8sn0dDIbwWZz","issuedAt":"2025-02-22T09:37:59.804Z","chainId":11155111,"resources":["http://127.0.0.1:5173/api/chat","http://127.0.0.1:5173/api/contract"]},"authenticated":true}
|
||||||
@@ -195,9 +195,9 @@
|
|||||||
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
|
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
|
||||||
|
|
||||||
"@langchain/community@^0.3.31":
|
"@langchain/community@^0.3.31":
|
||||||
version "0.3.31"
|
version "0.3.32"
|
||||||
resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.31.tgz#6c7a52091a4f26fd044ce04b059dec083d1ab0d2"
|
resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.32.tgz#aa415ebfd51f10610f3abaa4127dae197f29cdc7"
|
||||||
integrity sha512-VsqZBQdJqJ8LeBjmVAAujpGQaSYjSXItb8uyWzoq/+fQkpq7UjWGnKWDkY65kBYPNDf6ShwjUdGiLm2yuyefzQ==
|
integrity sha512-5AvGyjIFheXdBUSiIWNwc40rI8fXYiHV0UA3ncbBVu5fTwWur+mAQvl2ZsgyxBBKm4VuoCcuh6U6I7b1kiOYBQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@langchain/openai" ">=0.2.0 <0.5.0"
|
"@langchain/openai" ">=0.2.0 <0.5.0"
|
||||||
binary-extensions "^2.2.0"
|
binary-extensions "^2.2.0"
|
||||||
@@ -612,9 +612,9 @@
|
|||||||
form-data "^4.0.0"
|
form-data "^4.0.0"
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "22.13.4"
|
version "22.13.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.4.tgz#3fe454d77cd4a2d73c214008b3e331bfaaf5038a"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.5.tgz#23add1d71acddab2c6a4d31db89c0f98d330b511"
|
||||||
integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==
|
integrity sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~6.20.0"
|
undici-types "~6.20.0"
|
||||||
|
|
||||||
@@ -940,7 +940,7 @@ bytes@3.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||||
|
|
||||||
call-bind-apply-helpers@^1.0.1:
|
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||||
@@ -1270,7 +1270,7 @@ es-errors@^1.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||||
|
|
||||||
es-object-atoms@^1.0.0:
|
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||||
@@ -1584,22 +1584,22 @@ get-func-name@^2.0.0, get-func-name@^2.0.1, get-func-name@^2.0.2:
|
|||||||
integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==
|
integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==
|
||||||
|
|
||||||
get-intrinsic@^1.2.5, get-intrinsic@^1.2.6:
|
get-intrinsic@^1.2.5, get-intrinsic@^1.2.6:
|
||||||
version "1.2.7"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044"
|
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||||
integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==
|
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers "^1.0.1"
|
call-bind-apply-helpers "^1.0.2"
|
||||||
es-define-property "^1.0.1"
|
es-define-property "^1.0.1"
|
||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
es-object-atoms "^1.0.0"
|
es-object-atoms "^1.1.1"
|
||||||
function-bind "^1.1.2"
|
function-bind "^1.1.2"
|
||||||
get-proto "^1.0.0"
|
get-proto "^1.0.1"
|
||||||
gopd "^1.2.0"
|
gopd "^1.2.0"
|
||||||
has-symbols "^1.1.0"
|
has-symbols "^1.1.0"
|
||||||
hasown "^2.0.2"
|
hasown "^2.0.2"
|
||||||
math-intrinsics "^1.1.0"
|
math-intrinsics "^1.1.0"
|
||||||
|
|
||||||
get-proto@^1.0.0:
|
get-proto@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||||
@@ -2210,9 +2210,9 @@ once@^1.3.0:
|
|||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
openai@^4.77.0, openai@^4.85.2:
|
openai@^4.77.0, openai@^4.85.2:
|
||||||
version "4.85.2"
|
version "4.85.4"
|
||||||
resolved "https://registry.yarnpkg.com/openai/-/openai-4.85.2.tgz#370702c56cd61c17c44c8c02c6a48229d5299903"
|
resolved "https://registry.yarnpkg.com/openai/-/openai-4.85.4.tgz#fdf9d3228967b87221a112993501fa33a98f5d18"
|
||||||
integrity sha512-ZQg3Q+K4A6M9dLFh5W36paZkZBQO+VbxMNJ1gUSyHsGiEWuXahdn02ermqNV68LhWQxdJQaWUFRAYpW/suTPWQ==
|
integrity sha512-Nki51PBSu+Aryo7WKbdXvfm0X/iKkQS2fq3O0Uqb/O3b4exOZFid2te1BZ52bbO5UwxQZ5eeHJDCTqtrJLPw0w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "^18.11.18"
|
"@types/node" "^18.11.18"
|
||||||
"@types/node-fetch" "^2.6.4"
|
"@types/node-fetch" "^2.6.4"
|
||||||
@@ -2800,9 +2800,9 @@ supports-color@^8.1.1:
|
|||||||
has-flag "^4.0.0"
|
has-flag "^4.0.0"
|
||||||
|
|
||||||
tinyglobby@^0.2.6:
|
tinyglobby@^0.2.6:
|
||||||
version "0.2.11"
|
version "0.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.11.tgz#9182cff655a0e272aad850d1a84c5e8e0f700426"
|
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5"
|
||||||
integrity sha512-32TmKeeKUahv0Go8WmQgiEp9Y21NuxjwjqiRC1nrUB51YacfSwuB44xgXD+HdIppmMRgjQNPdrHyA6vIybYZ+g==
|
integrity sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir "^6.4.3"
|
fdir "^6.4.3"
|
||||||
picomatch "^4.0.2"
|
picomatch "^4.0.2"
|
||||||
@@ -3083,9 +3083,9 @@ yocto-queue@^0.1.0:
|
|||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.5, zod-to-json-schema@^3.24.1:
|
zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.5, zod-to-json-schema@^3.24.1:
|
||||||
version "3.24.2"
|
version "3.24.3"
|
||||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.2.tgz#0e24e4a963ab34cf4211ef5227e342c0c6eddb79"
|
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz#5958ba111d681f8d01c5b6b647425c9b8a6059e7"
|
||||||
integrity sha512-pNUqrcSxuuB3/+jBbU8qKUbTbDqYUaG1vf5cXFjbhGgoUuA1amO/y4Q8lzfOhHU8HNPK6VFJ18lBDKj3OHyDsg==
|
integrity sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==
|
||||||
|
|
||||||
zod@^3.22.3, zod@^3.22.4, zod@^3.24.1:
|
zod@^3.22.3, zod@^3.22.4, zod@^3.24.1:
|
||||||
version "3.24.2"
|
version "3.24.2"
|
||||||
|
|||||||
@@ -2,8 +2,19 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>DApp for Business</title>
|
<title>DApp for Business</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "1.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -9,11 +9,12 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ethers": "^6.13.5",
|
"ethers": "^6.11.1",
|
||||||
"vue": "^3.4.21"
|
"vue": "^3.4.15",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
"vite": "^5.1.7"
|
"vite": "^5.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,149 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<h1>DApp for Business</h1>
|
<div class="app-layout">
|
||||||
<ContractInteraction ref="contractInteraction" />
|
<Sidebar
|
||||||
<AIAssistant
|
v-if="isConnected && isAdmin"
|
||||||
:isConnected="isConnected"
|
:isAdmin="isAdmin"
|
||||||
:userAddress="userAddress"
|
@update:collapsed="isSidebarCollapsed = $event"
|
||||||
@chatUpdated="handleChatUpdate"
|
/>
|
||||||
/>
|
<main class="main-content">
|
||||||
<ServerControl />
|
<WalletConnection
|
||||||
<DataTables
|
:isConnected="isConnected"
|
||||||
:isConnected="isConnected"
|
:userAddress="userAddress"
|
||||||
:userAddress="userAddress"
|
@connect="handleConnect"
|
||||||
ref="dataTables"
|
@disconnect="handleDisconnect"
|
||||||
/>
|
/>
|
||||||
|
<router-view
|
||||||
|
:userAddress="userAddress"
|
||||||
|
:isConnected="isConnected"
|
||||||
|
:isAdmin="isAdmin"
|
||||||
|
@chatUpdated="handleChatUpdate"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import ContractInteraction from './components/ContractInteraction.vue'
|
import { useRouter } from 'vue-router'
|
||||||
import AIAssistant from './components/AIAssistant.vue'
|
import WalletConnection from './components/WalletConnection.vue'
|
||||||
import ServerControl from './components/ServerControl.vue'
|
import Sidebar from './components/Sidebar/Sidebar.vue'
|
||||||
import DataTables from './components/DataTables.vue'
|
|
||||||
|
|
||||||
const contractInteraction = ref(null)
|
const router = useRouter()
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
const userAddress = ref(null)
|
const userAddress = ref('')
|
||||||
const dataTables = ref(null)
|
const isAdmin = ref(false)
|
||||||
|
const isSidebarCollapsed = ref(false)
|
||||||
|
|
||||||
watch(() => contractInteraction.value?.isConnected, (newValue) => {
|
// При отключении кошелька перенаправляем на главную
|
||||||
isConnected.value = newValue
|
watch(isConnected, (newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => contractInteraction.value?.address, (newValue) => {
|
// Проверка сессии при загрузке
|
||||||
userAddress.value = newValue
|
async function checkSession() {
|
||||||
})
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:3000/session', {
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.authenticated) {
|
||||||
|
userAddress.value = data.address
|
||||||
|
isConnected.value = true
|
||||||
|
await checkAdminStatus()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки сессии:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка прав админа
|
||||||
|
async function checkAdminStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:3000/api/admin/check', {
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
if (response.ok) {
|
||||||
|
const { isAdmin: adminStatus } = await response.json()
|
||||||
|
isAdmin.value = adminStatus
|
||||||
|
console.log('Проверка прав после входа:', {
|
||||||
|
userAddress: userAddress.value,
|
||||||
|
isAdmin: isAdmin.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки прав админа:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConnect(address) {
|
||||||
|
userAddress.value = address
|
||||||
|
isConnected.value = true
|
||||||
|
await checkAdminStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDisconnect() {
|
||||||
|
try {
|
||||||
|
// Отправляем запрос на выход
|
||||||
|
await fetch('http://127.0.0.1:3000/signout', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
|
||||||
|
userAddress.value = ''
|
||||||
|
isConnected.value = false
|
||||||
|
isAdmin.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при отключении:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleChatUpdate() {
|
function handleChatUpdate() {
|
||||||
dataTables.value?.fetchData()
|
if (isAdmin.value) {
|
||||||
|
// Обновляем данные в админской панели
|
||||||
|
// dataTables.value?.fetchData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkSession()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#app {
|
#app {
|
||||||
font-family: Arial, sans-serif;
|
height: 100vh;
|
||||||
-webkit-font-smoothing: antialiased;
|
overflow: hidden;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
}
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
.app-layout {
|
||||||
margin-top: 60px;
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.5rem 2rem; /* Соответствует отступам header */
|
||||||
|
margin-left: v-bind("isConnected && isAdmin ? (isSidebarCollapsed ? '80px' : '270px') : '0'");
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: margin-left 0.3s ease;
|
||||||
|
margin-top: 70px; /* Высота header + верхний отступ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Когда сайдбар скрыт */
|
||||||
|
.app-layout:not(:has(.sidebar)) .main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 2rem; /* Сохраняем отступ слева когда сайдбар скрыт */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для заголовка */
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
146
frontend/src/artifacts/MyContract.json
Normal file
146
frontend/src/artifacts/MyContract.json
Normal file
File diff suppressed because one or more lines are too long
238
frontend/src/components/AI/Chats.vue
Normal file
238
frontend/src/components/AI/Chats.vue
Normal 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>
|
||||||
281
frontend/src/components/AI/Users.vue
Normal file
281
frontend/src/components/AI/Users.vue
Normal 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>
|
||||||
254
frontend/src/components/AI/VectorStore.vue
Normal file
254
frontend/src/components/AI/VectorStore.vue
Normal 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>
|
||||||
@@ -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>
|
|
||||||
162
frontend/src/components/Contract/Deploy.vue
Normal file
162
frontend/src/components/Contract/Deploy.vue
Normal 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>
|
||||||
771
frontend/src/components/Contract/Manage.vue
Normal file
771
frontend/src/components/Contract/Manage.vue
Normal 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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
180
frontend/src/components/Sidebar/Sidebar.vue
Normal file
180
frontend/src/components/Sidebar/Sidebar.vue
Normal 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>
|
||||||
|
|
||||||
73
frontend/src/components/Sidebar/SidebarItem.vue
Normal file
73
frontend/src/components/Sidebar/SidebarItem.vue
Normal 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>
|
||||||
243
frontend/src/components/WalletConnection.vue
Normal file
243
frontend/src/components/WalletConnection.vue
Normal 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>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import router from './router/index.js'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
// Создаем и монтируем приложение Vue
|
// Создаем и монтируем приложение Vue
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
63
frontend/src/router/index.js
Normal file
63
frontend/src/router/index.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Chats from '../components/AI/Chats.vue'
|
||||||
|
import Users from '../components/AI/Users.vue'
|
||||||
|
import VectorStore from '../components/AI/VectorStore.vue'
|
||||||
|
import Deploy from '../components/Contract/Deploy.vue'
|
||||||
|
import Manage from '../components/Contract/Manage.vue'
|
||||||
|
|
||||||
|
// Защита маршрутов для админа
|
||||||
|
const requireAdmin = async (to, from, next) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:3000/api/admin/check', {
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
if (response.ok) {
|
||||||
|
const { isAdmin } = await response.json()
|
||||||
|
if (isAdmin) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next('/')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки прав:', error)
|
||||||
|
next('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/ai/chats'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ai/chats',
|
||||||
|
name: 'chats',
|
||||||
|
component: Chats
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ai/users',
|
||||||
|
beforeEnter: requireAdmin,
|
||||||
|
component: Users
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ai/vectorstore',
|
||||||
|
beforeEnter: requireAdmin,
|
||||||
|
component: VectorStore
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/contract/deploy',
|
||||||
|
beforeEnter: requireAdmin,
|
||||||
|
component: Deploy
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/contract/manage',
|
||||||
|
beforeEnter: requireAdmin,
|
||||||
|
component: Manage
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~6.19.2"
|
undici-types "~6.19.2"
|
||||||
|
|
||||||
"@vitejs/plugin-vue@^5.0.4":
|
"@vitejs/plugin-vue@^5.0.3":
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz#d1491f678ee3af899f7ae57d9c21dc52a65c7133"
|
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz#d1491f678ee3af899f7ae57d9c21dc52a65c7133"
|
||||||
integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==
|
integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==
|
||||||
@@ -318,6 +318,11 @@
|
|||||||
"@vue/compiler-dom" "3.5.13"
|
"@vue/compiler-dom" "3.5.13"
|
||||||
"@vue/shared" "3.5.13"
|
"@vue/shared" "3.5.13"
|
||||||
|
|
||||||
|
"@vue/devtools-api@^6.6.4":
|
||||||
|
version "6.6.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||||
|
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||||
|
|
||||||
"@vue/reactivity@3.5.13":
|
"@vue/reactivity@3.5.13":
|
||||||
version "3.5.13"
|
version "3.5.13"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f"
|
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f"
|
||||||
@@ -405,7 +410,7 @@ estree-walker@^2.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
||||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||||
|
|
||||||
ethers@^6.13.5:
|
ethers@^6.11.1:
|
||||||
version "6.13.5"
|
version "6.13.5"
|
||||||
resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.5.tgz#8c1d6ac988ac08abc3c1d8fabbd4b8b602851ac4"
|
resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.5.tgz#8c1d6ac988ac08abc3c1d8fabbd4b8b602851ac4"
|
||||||
integrity sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==
|
integrity sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==
|
||||||
@@ -492,7 +497,7 @@ undici-types@~6.19.2:
|
|||||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
|
||||||
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
||||||
|
|
||||||
vite@^5.1.7:
|
vite@^5.0.11:
|
||||||
version "5.4.14"
|
version "5.4.14"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
|
||||||
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
|
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
|
||||||
@@ -503,7 +508,14 @@ vite@^5.1.7:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.3"
|
fsevents "~2.3.3"
|
||||||
|
|
||||||
vue@^3.4.21:
|
vue-router@^4.2.5:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14"
|
||||||
|
integrity sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==
|
||||||
|
dependencies:
|
||||||
|
"@vue/devtools-api" "^6.6.4"
|
||||||
|
|
||||||
|
vue@^3.4.15:
|
||||||
version "3.5.13"
|
version "3.5.13"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"
|
||||||
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
|
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
|
||||||
|
|||||||
Reference in New Issue
Block a user