ваше сообщение коммита
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE ai_providers_settings ADD COLUMN embedding_model VARCHAR(128);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE ai_assistant_settings ADD COLUMN embedding_model VARCHAR(128);
|
||||||
@@ -423,9 +423,8 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
const userMessage = userMessageResult.rows[0];
|
const userMessage = userMessageResult.rows[0];
|
||||||
logger.info('User message saved', { messageId: userMessage.id, conversationId });
|
logger.info('User message saved', { messageId: userMessage.id, conversationId });
|
||||||
|
|
||||||
// Получаем ответ от ИИ, только если это было текстовое сообщение
|
// --- Новая логика автоответа ИИ по RAG ---
|
||||||
let aiMessage = null;
|
let aiMessage = null;
|
||||||
// --- Новая логика автоответа ИИ ---
|
|
||||||
let shouldGenerateAiReply = true;
|
let shouldGenerateAiReply = true;
|
||||||
if (senderType === 'admin') {
|
if (senderType === 'admin') {
|
||||||
// Если админ пишет не себе, не отвечаем
|
// Если админ пишет не себе, не отвечаем
|
||||||
@@ -441,9 +440,34 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
if (aiSettings && aiSettings.rules_id) {
|
if (aiSettings && aiSettings.rules_id) {
|
||||||
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||||||
}
|
}
|
||||||
logger.info('AI System Prompt:', aiSettings ? aiSettings.system_prompt : 'not set');
|
// --- RAG автоответ ---
|
||||||
logger.info('AI Rules:', rules ? JSON.stringify(rules.rules) : 'not set');
|
let ragTableId = null;
|
||||||
// Получаем последние 10 сообщений из диалога для истории (до текущего сообщения)
|
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||||
|
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||||
|
? aiSettings.selected_rag_tables[0]
|
||||||
|
: aiSettings.selected_rag_tables;
|
||||||
|
}
|
||||||
|
let ragResult = null;
|
||||||
|
if (ragTableId) {
|
||||||
|
const { ragAnswer, generateLLMResponse } = require('../services/ragService');
|
||||||
|
const threshold = 0.3;
|
||||||
|
logger.info(`[RAG] Запуск поиска по RAG: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}`);
|
||||||
|
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: messageContent, threshold });
|
||||||
|
logger.info(`[RAG] Результат поиска по RAG:`, ragResult);
|
||||||
|
if (ragResult && ragResult.answer && ragResult.score && ragResult.score > threshold) {
|
||||||
|
logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
|
||||||
|
// Прямой ответ из RAG
|
||||||
|
const aiMessageResult = await db.getQuery()(
|
||||||
|
`INSERT INTO messages
|
||||||
|
(conversation_id, user_id, content, sender_type, role, channel)
|
||||||
|
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
|
||||||
|
RETURNING *`,
|
||||||
|
[conversationId, userId, ragResult.answer]
|
||||||
|
);
|
||||||
|
aiMessage = aiMessageResult.rows[0];
|
||||||
|
} else if (ragResult) {
|
||||||
|
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
|
||||||
|
// Генерация через LLM с подстановкой значений из RAG
|
||||||
const historyResult = await db.getQuery()(
|
const historyResult = await db.getQuery()(
|
||||||
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
|
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
|
||||||
[conversationId, userMessage.id]
|
[conversationId, userMessage.id]
|
||||||
@@ -452,30 +476,34 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||||
content: msg.content
|
content: msg.content
|
||||||
}));
|
}));
|
||||||
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(messageContent) : language;
|
const llmResponse = await generateLLMResponse({
|
||||||
logger.info('Getting AI response for:', messageContent);
|
userQuestion: messageContent,
|
||||||
const aiResponseContent = await aiAssistant.getResponse(
|
context: ragResult.context,
|
||||||
messageContent,
|
answer: ragResult.answer,
|
||||||
detectedLanguage,
|
clarifyingAnswer: ragResult.clarifyingAnswer,
|
||||||
|
objectionAnswer: ragResult.objectionAnswer,
|
||||||
|
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||||
history,
|
history,
|
||||||
aiSettings ? aiSettings.system_prompt : '',
|
model: aiSettings ? aiSettings.model : undefined,
|
||||||
rules ? rules.rules : null
|
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
|
||||||
);
|
});
|
||||||
logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), { conversationId });
|
if (llmResponse) {
|
||||||
|
|
||||||
if (aiResponseContent) {
|
|
||||||
const aiMessageResult = await db.getQuery()(
|
const aiMessageResult = await db.getQuery()(
|
||||||
`INSERT INTO messages
|
`INSERT INTO messages
|
||||||
(conversation_id, user_id, content, sender_type, role, channel)
|
(conversation_id, user_id, content, sender_type, role, channel)
|
||||||
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
|
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[conversationId, userId, aiResponseContent]
|
[conversationId, userId, llmResponse]
|
||||||
);
|
);
|
||||||
aiMessage = aiMessageResult.rows[0];
|
aiMessage = aiMessageResult.rows[0];
|
||||||
logger.info('AI response saved', { messageId: aiMessage.id, conversationId });
|
} else {
|
||||||
|
logger.info(`[RAG] Нет ни одного результата, прошедшего порог (${threshold}).`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- конец RAG автоответа ---
|
||||||
} catch (aiError) {
|
} catch (aiError) {
|
||||||
logger.error('Error getting or saving AI response:', aiError);
|
logger.error('Error getting or saving AI response (RAG):', aiError);
|
||||||
// Не прерываем основной ответ, но логируем ошибку
|
// Не прерываем основной ответ, но логируем ошибку
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -702,14 +730,32 @@ router.post('/ai-draft', requireAuth, async (req, res) => {
|
|||||||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||||
content: msg.content
|
content: msg.content
|
||||||
}));
|
}));
|
||||||
|
// --- RAG draft ---
|
||||||
|
let ragTableId = null;
|
||||||
|
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||||
|
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||||
|
? aiSettings.selected_rag_tables[0]
|
||||||
|
: aiSettings.selected_rag_tables;
|
||||||
|
}
|
||||||
|
let ragResult = null;
|
||||||
|
if (ragTableId) {
|
||||||
|
const { ragAnswer } = require('../services/ragService');
|
||||||
|
logger.info(`[RAG] [DRAFT] Запуск поиска по RAG: tableId=${ragTableId}, draft prompt="${promptText}"`);
|
||||||
|
ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: promptText });
|
||||||
|
logger.info(`[RAG] [DRAFT] Результат поиска по RAG:`, ragResult);
|
||||||
|
}
|
||||||
|
const { generateLLMResponse } = require('../services/ragService');
|
||||||
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(promptText) : language;
|
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(promptText) : language;
|
||||||
const aiResponseContent = await aiAssistant.getResponse(
|
const aiResponseContent = await generateLLMResponse({
|
||||||
promptText,
|
userQuestion: promptText,
|
||||||
detectedLanguage,
|
context: ragResult && ragResult.context ? ragResult.context : '',
|
||||||
|
answer: ragResult && ragResult.answer ? ragResult.answer : '',
|
||||||
|
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||||
history,
|
history,
|
||||||
aiSettings ? aiSettings.system_prompt : '',
|
model: aiSettings ? aiSettings.model : undefined,
|
||||||
rules ? rules.rules : null
|
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru',
|
||||||
);
|
rules: rules ? rules.rules : null
|
||||||
|
});
|
||||||
res.json({ success: true, aiMessage: aiResponseContent });
|
res.json({ success: true, aiMessage: aiResponseContent });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error generating AI draft:', error);
|
logger.error('Error generating AI draft:', error);
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ const aiAssistant = require('../services/ai-assistant');
|
|||||||
const dns = require('node:dns').promises;
|
const dns = require('node:dns').promises;
|
||||||
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
|
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
|
||||||
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
||||||
|
const telegramBot = require('../services/telegramBot');
|
||||||
|
const EmailBotService = require('../services/emailBot');
|
||||||
|
const emailBotService = new EmailBotService();
|
||||||
|
const dbSettingsService = require('../services/dbSettingsService');
|
||||||
|
|
||||||
// Логируем версию ethers для отладки
|
// Логируем версию ethers для отладки
|
||||||
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
|
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
|
||||||
@@ -185,8 +189,8 @@ router.get('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
|
|||||||
router.put('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
|
router.put('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { provider } = req.params;
|
const { provider } = req.params;
|
||||||
const { api_key, base_url, selected_model } = req.body;
|
const { api_key, base_url, selected_model, embedding_model } = req.body;
|
||||||
const updated = await aiProviderSettingsService.upsertProviderSettings({ provider, api_key, base_url, selected_model });
|
const updated = await aiProviderSettingsService.upsertProviderSettings({ provider, api_key, base_url, selected_model, embedding_model });
|
||||||
res.json({ success: true, settings: updated });
|
res.json({ success: true, settings: updated });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при сохранении AI-настроек:', error);
|
logger.error('Ошибка при сохранении AI-настроек:', error);
|
||||||
@@ -252,7 +256,25 @@ router.get('/ai-assistant', requireAdmin, async (req, res, next) => {
|
|||||||
|
|
||||||
router.put('/ai-assistant', requireAdmin, async (req, res, next) => {
|
router.put('/ai-assistant', requireAdmin, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const updated = await aiAssistantSettingsService.upsertSettings({ ...req.body, updated_by: req.session.userId || null });
|
let { selected_rag_tables, ...rest } = req.body;
|
||||||
|
// Приведение к массиву чисел
|
||||||
|
if (typeof selected_rag_tables === 'string') {
|
||||||
|
try {
|
||||||
|
selected_rag_tables = JSON.parse(selected_rag_tables);
|
||||||
|
} catch {
|
||||||
|
selected_rag_tables = [Number(selected_rag_tables)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Array.isArray(selected_rag_tables)) {
|
||||||
|
selected_rag_tables = [Number(selected_rag_tables)];
|
||||||
|
}
|
||||||
|
selected_rag_tables = selected_rag_tables.map(Number);
|
||||||
|
|
||||||
|
const updated = await aiAssistantSettingsService.upsertSettings({
|
||||||
|
...rest,
|
||||||
|
selected_rag_tables,
|
||||||
|
updated_by: req.session.userId || null
|
||||||
|
});
|
||||||
res.json({ success: true, settings: updated });
|
res.json({ success: true, settings: updated });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -309,23 +331,101 @@ router.delete('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получить все email_settings для выпадающего списка
|
// Получить текущие настройки Email (для страницы Email)
|
||||||
router.get('/email-settings', requireAdmin, async (req, res, next) => {
|
router.get('/email-settings', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { rows } = await require('../db').getQuery()('SELECT id, from_email FROM email_settings ORDER BY id');
|
const settings = await emailBotService.getSettingsFromDb();
|
||||||
res.json({ success: true, items: rows });
|
res.json({ success: true, settings });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить список всех email (для ассистента)
|
||||||
|
router.get('/email-settings/list', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const emails = await emailBotService.getAllEmailSettings();
|
||||||
|
res.json({ success: true, items: emails });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить текущие настройки Telegram-бота (для страницы Telegram)
|
||||||
|
router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const settings = await telegramBot.getTelegramSettings();
|
||||||
|
res.json({ success: true, settings });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить список всех Telegram-ботов (для ассистента)
|
||||||
|
router.get('/telegram-settings/list', requireAdmin, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const bots = await telegramBot.getAllBots();
|
||||||
|
res.json({ success: true, items: bots });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение списка моделей для выбранного AI-провайдера
|
||||||
|
router.get('/ai-provider-models', requireAdmin, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const provider = req.query.provider;
|
||||||
|
if (!provider) return res.status(400).json({ error: 'provider is required' });
|
||||||
|
const settings = await aiProviderSettingsService.getProviderSettings(provider);
|
||||||
|
if (!settings) return res.status(404).json({ error: 'Provider not found' });
|
||||||
|
const models = await aiProviderSettingsService.getProviderModels(provider, {
|
||||||
|
api_key: settings.api_key,
|
||||||
|
base_url: settings.base_url,
|
||||||
|
});
|
||||||
|
res.json({ models });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получить все telegram_settings для выпадающего списка
|
// Получить настройки базы данных
|
||||||
router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
|
router.get('/db-settings', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { rows } = await require('../db').getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id');
|
const settings = await dbSettingsService.getSettings();
|
||||||
res.json({ success: true, items: rows });
|
res.json({ success: true, settings });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
res.status(404).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновить настройки базы данных
|
||||||
|
router.put('/db-settings', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { db_host, db_port, db_name, db_user, db_password } = req.body;
|
||||||
|
const updated = await dbSettingsService.upsertSettings({ db_host, db_port, db_name, db_user, db_password });
|
||||||
|
res.json({ success: true, settings: updated });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить все LLM-модели
|
||||||
|
router.get('/llm-models', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const models = await aiProviderSettingsService.getAllLLMModels();
|
||||||
|
res.json({ success: true, models });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить все embedding-модели
|
||||||
|
router.get('/embedding-models', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const models = await aiProviderSettingsService.getAllEmbeddingModels();
|
||||||
|
res.json({ success: true, models });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ router.post('/:id/columns', async (req, res, next) => {
|
|||||||
try {
|
try {
|
||||||
const tableId = req.params.id;
|
const tableId = req.params.id;
|
||||||
const { name, type, options, order, tagIds, purpose } = req.body;
|
const { name, type, options, order, tagIds, purpose } = req.body;
|
||||||
let finalOptions = options;
|
let finalOptions = options || {};
|
||||||
// Собираем options
|
|
||||||
finalOptions = finalOptions || {};
|
|
||||||
if (type === 'tags' && Array.isArray(tagIds)) {
|
if (type === 'tags' && Array.isArray(tagIds)) {
|
||||||
finalOptions.tagIds = tagIds;
|
finalOptions.tagIds = tagIds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,19 +20,21 @@ async function getSettings() {
|
|||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
telegramBot,
|
telegramBot,
|
||||||
supportEmail
|
supportEmail,
|
||||||
|
embedding_model: settings.embedding_model
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
|
async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
|
||||||
const { rows } = await db.getQuery()(
|
const { rows } = await db.getQuery()(
|
||||||
`INSERT INTO ${TABLE} (id, system_prompt, selected_rag_tables, languages, model, rules, updated_at, updated_by, telegram_settings_id, email_settings_id, system_message)
|
`INSERT INTO ${TABLE} (id, system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_at, updated_by, telegram_settings_id, email_settings_id, system_message)
|
||||||
VALUES (1, $1, $2, $3, $4, $5, NOW(), $6, $7, $8, $9)
|
VALUES (1, $1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
system_prompt = EXCLUDED.system_prompt,
|
system_prompt = EXCLUDED.system_prompt,
|
||||||
selected_rag_tables = EXCLUDED.selected_rag_tables,
|
selected_rag_tables = EXCLUDED.selected_rag_tables,
|
||||||
languages = EXCLUDED.languages,
|
languages = EXCLUDED.languages,
|
||||||
model = EXCLUDED.model,
|
model = EXCLUDED.model,
|
||||||
|
embedding_model = EXCLUDED.embedding_model,
|
||||||
rules = EXCLUDED.rules,
|
rules = EXCLUDED.rules,
|
||||||
updated_at = NOW(),
|
updated_at = NOW(),
|
||||||
updated_by = EXCLUDED.updated_by,
|
updated_by = EXCLUDED.updated_by,
|
||||||
@@ -40,7 +42,7 @@ async function upsertSettings({ system_prompt, selected_rag_tables, languages, m
|
|||||||
email_settings_id = EXCLUDED.email_settings_id,
|
email_settings_id = EXCLUDED.email_settings_id,
|
||||||
system_message = EXCLUDED.system_message
|
system_message = EXCLUDED.system_message
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[system_prompt, selected_rag_tables, languages, model, rules, updated_by, telegram_settings_id, email_settings_id, system_message]
|
[system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message]
|
||||||
);
|
);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,17 +12,18 @@ async function getProviderSettings(provider) {
|
|||||||
return rows[0] || null;
|
return rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertProviderSettings({ provider, api_key, base_url, selected_model }) {
|
async function upsertProviderSettings({ provider, api_key, base_url, selected_model, embedding_model }) {
|
||||||
const { rows } = await db.getQuery()(
|
const { rows } = await db.getQuery()(
|
||||||
`INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, updated_at)
|
`INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, embedding_model, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, NOW())
|
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||||
ON CONFLICT (provider) DO UPDATE SET
|
ON CONFLICT (provider) DO UPDATE SET
|
||||||
api_key = EXCLUDED.api_key,
|
api_key = EXCLUDED.api_key,
|
||||||
base_url = EXCLUDED.base_url,
|
base_url = EXCLUDED.base_url,
|
||||||
selected_model = EXCLUDED.selected_model,
|
selected_model = EXCLUDED.selected_model,
|
||||||
|
embedding_model = EXCLUDED.embedding_model,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[provider, api_key, base_url, selected_model]
|
[provider, api_key, base_url, selected_model, embedding_model]
|
||||||
);
|
);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
@@ -97,10 +98,28 @@ async function verifyProviderKey(provider, { api_key, base_url } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAllLLMModels() {
|
||||||
|
const { rows } = await db.getQuery()(
|
||||||
|
`SELECT provider, selected_model FROM ${TABLE} WHERE selected_model IS NOT NULL AND selected_model <> ''`
|
||||||
|
);
|
||||||
|
// Возвращаем массив объектов { id, provider }
|
||||||
|
return rows.map(r => ({ id: r.selected_model, provider: r.provider }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllEmbeddingModels() {
|
||||||
|
const { rows } = await db.getQuery()(
|
||||||
|
`SELECT provider, embedding_model FROM ${TABLE} WHERE embedding_model IS NOT NULL AND embedding_model <> ''`
|
||||||
|
);
|
||||||
|
// Возвращаем массив объектов { id, provider }
|
||||||
|
return rows.map(r => ({ id: r.embedding_model, provider: r.provider }));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getProviderSettings,
|
getProviderSettings,
|
||||||
upsertProviderSettings,
|
upsertProviderSettings,
|
||||||
deleteProviderSettings,
|
deleteProviderSettings,
|
||||||
getProviderModels,
|
getProviderModels,
|
||||||
verifyProviderKey,
|
verifyProviderKey,
|
||||||
|
getAllLLMModels,
|
||||||
|
getAllEmbeddingModels,
|
||||||
};
|
};
|
||||||
27
backend/services/dbSettingsService.js
Normal file
27
backend/services/dbSettingsService.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const db = require('../db');
|
||||||
|
|
||||||
|
class DbSettingsService {
|
||||||
|
async getSettings() {
|
||||||
|
const { rows } = await db.getQuery()('SELECT * FROM db_settings WHERE id = 1');
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsertSettings({ db_host, db_port, db_name, db_user, db_password }) {
|
||||||
|
const { rows } = await db.getQuery()(
|
||||||
|
`INSERT INTO db_settings (id, db_host, db_port, db_name, db_user, db_password, updated_at)
|
||||||
|
VALUES (1, $1, $2, $3, $4, $5, NOW())
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
db_host = EXCLUDED.db_host,
|
||||||
|
db_port = EXCLUDED.db_port,
|
||||||
|
db_name = EXCLUDED.db_name,
|
||||||
|
db_user = EXCLUDED.db_user,
|
||||||
|
db_password = EXCLUDED.db_password,
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING *`,
|
||||||
|
[db_host, db_port, db_name, db_user, db_password]
|
||||||
|
);
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new DbSettingsService();
|
||||||
@@ -309,6 +309,11 @@ class EmailBotService {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllEmailSettings() {
|
||||||
|
const { rows } = await db.getQuery()('SELECT id, from_email FROM email_settings ORDER BY id');
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[EmailBot] module.exports = EmailBotService');
|
console.log('[EmailBot] module.exports = EmailBotService');
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
const { OpenAIEmbeddings } = require('@langchain/openai');
|
|
||||||
const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib');
|
const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib');
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const { ChatOllama } = require('@langchain/ollama');
|
const { ChatOllama } = require('@langchain/ollama');
|
||||||
|
const { OllamaEmbeddings } = require('@langchain/ollama');
|
||||||
|
const { getProviderSettings } = require('./aiProviderSettingsService');
|
||||||
|
const { OpenAIEmbeddings } = require('@langchain/openai');
|
||||||
|
|
||||||
|
console.log('[RAG] ragService.js loaded');
|
||||||
|
|
||||||
async function getTableData(tableId) {
|
async function getTableData(tableId) {
|
||||||
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
|
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
|
||||||
|
console.log('RAG getTableData: columns:', columns);
|
||||||
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1', [tableId])).rows;
|
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1', [tableId])).rows;
|
||||||
const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
|
const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
|
||||||
|
|
||||||
@@ -17,7 +22,7 @@ async function getTableData(tableId) {
|
|||||||
const priorityColId = getColId('priority');
|
const priorityColId = getColId('priority');
|
||||||
const dateColId = getColId('date');
|
const dateColId = getColId('date');
|
||||||
|
|
||||||
return rows.map(row => {
|
const data = rows.map(row => {
|
||||||
const cells = cellValues.filter(cell => cell.row_id === row.id);
|
const cells = cellValues.filter(cell => cell.row_id === row.id);
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -30,35 +35,107 @@ async function getTableData(tableId) {
|
|||||||
date: cells.find(c => c.column_id === dateColId)?.value,
|
date: cells.find(c => c.column_id === dateColId)?.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const questions = data.map(row => row.question);
|
||||||
|
console.log('RAG getTableData: questions:', questions);
|
||||||
|
if (!questions.length) {
|
||||||
|
console.warn('RAG getTableData: questions array is empty! Проверьте структуру колонок и наличие данных.');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ragAnswer({ tableId, userQuestion, userTags = [], product = null }) {
|
async function getEmbeddingsProvider(providerName = 'ollama') {
|
||||||
|
const settings = await getProviderSettings(providerName);
|
||||||
|
if (!settings) throw new Error('Embeddings provider settings not found');
|
||||||
|
switch (providerName) {
|
||||||
|
case 'openai':
|
||||||
|
return new OpenAIEmbeddings({
|
||||||
|
apiKey: settings.api_key,
|
||||||
|
baseURL: settings.base_url,
|
||||||
|
model: settings.selected_model || undefined,
|
||||||
|
});
|
||||||
|
case 'ollama': {
|
||||||
|
// Fallback: если не задан base_url, пробуем env, host.docker.internal, localhost
|
||||||
|
let baseUrl = settings.base_url;
|
||||||
|
if (!baseUrl) {
|
||||||
|
baseUrl = process.env.OLLAMA_BASE_URL;
|
||||||
|
}
|
||||||
|
if (!baseUrl) {
|
||||||
|
// Если в Docker — используем host.docker.internal
|
||||||
|
baseUrl = 'http://host.docker.internal:11434';
|
||||||
|
}
|
||||||
|
// Если всё равно нет — последний fallback
|
||||||
|
if (!baseUrl) {
|
||||||
|
baseUrl = 'http://localhost:11434';
|
||||||
|
}
|
||||||
|
return new OllamaEmbeddings({
|
||||||
|
model: settings.embedding_model || process.env.OLLAMA_EMBED_MODEL || 'mxbai-embed-large',
|
||||||
|
baseUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// case 'gemini':
|
||||||
|
// return new GeminiEmbeddings({ apiKey: settings.api_key });
|
||||||
|
// Добавьте другие провайдеры по аналогии
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown embeddings provider: ' + providerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ragAnswer({ tableId, userQuestion, userTags = [], product = null, embeddingsProvider = 'ollama', threshold = 0.3 }) {
|
||||||
|
console.log('[RAG] Используется провайдер эмбеддингов:', embeddingsProvider);
|
||||||
const data = await getTableData(tableId);
|
const data = await getTableData(tableId);
|
||||||
const questions = data.map(row => row.question);
|
// Триммируем вопросы для чистоты сравнения
|
||||||
|
const questions = data.map(row => row.question && typeof row.question === 'string' ? row.question.trim() : row.question);
|
||||||
|
|
||||||
|
// Получаем embeddings-инстанс динамически
|
||||||
|
const embeddingsInstance = await getEmbeddingsProvider(embeddingsProvider);
|
||||||
|
|
||||||
// Получаем embedding для всех вопросов
|
// Получаем embedding для всех вопросов
|
||||||
const embeddings = await new OpenAIEmbeddings().embedDocuments(questions);
|
const embeddings = await embeddingsInstance.embedDocuments(questions);
|
||||||
|
console.log('Questions embedding length:', embeddings[0]?.length, 'Total questions:', questions.length);
|
||||||
|
|
||||||
|
// Получаем embedding для вопроса пользователя (trim)
|
||||||
|
const userQuestionTrimmed = userQuestion && typeof userQuestion === 'string' ? userQuestion.trim() : userQuestion;
|
||||||
|
const [userEmbedding] = await embeddingsInstance.embedDocuments([userQuestionTrimmed]);
|
||||||
|
console.log('User embedding length:', userEmbedding?.length, 'User question:', userQuestionTrimmed);
|
||||||
|
|
||||||
|
// Явно сравниваем embeddings (отладка)
|
||||||
|
console.log('[RAG] Embedding сравнение:');
|
||||||
|
embeddings.forEach((emb, idx) => {
|
||||||
|
const dot = emb.reduce((sum, v, i) => sum + v * userEmbedding[i], 0);
|
||||||
|
console.log(` [${idx}] dot-product: ${dot} | question: "${questions[idx]}"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создаём массив метаданных для каждого вопроса
|
||||||
|
const metadatas = data.map(row => ({
|
||||||
|
id: row.id,
|
||||||
|
answer: row.answer,
|
||||||
|
userTags: row.userTags,
|
||||||
|
context: row.context,
|
||||||
|
product: row.product,
|
||||||
|
priority: row.priority,
|
||||||
|
date: row.date,
|
||||||
|
}));
|
||||||
|
|
||||||
// Создаём векторное хранилище
|
// Создаём векторное хранилище
|
||||||
const vectorStore = await HNSWLib.fromTexts(questions, data, new OpenAIEmbeddings());
|
const vectorStore = await HNSWLib.fromTexts(questions, metadatas, embeddingsInstance);
|
||||||
|
|
||||||
// Получаем embedding для вопроса пользователя
|
|
||||||
const [userEmbedding] = await new OpenAIEmbeddings().embedDocuments([userQuestion]);
|
|
||||||
|
|
||||||
// Ищем наиболее похожие вопросы (top-3)
|
// Ищем наиболее похожие вопросы (top-3)
|
||||||
const results = await vectorStore.similaritySearchVectorWithScore(userEmbedding, 3);
|
const results = await vectorStore.similaritySearchVectorWithScore(userEmbedding, 3);
|
||||||
|
console.log('[RAG] Результаты поиска по векторам (score):', results.map(([doc, score]) => ({ ...doc.metadata, score })));
|
||||||
|
|
||||||
// Фильтруем по тегам/продукту, если нужно
|
// Фильтруем по тегам/продукту, если нужно
|
||||||
let filtered = results.map(([row, score]) => ({ ...row, score }));
|
let filtered = results.map(([doc, score]) => ({ ...doc.metadata, score }));
|
||||||
if (userTags.length) {
|
if (userTags.length) {
|
||||||
filtered = filtered.filter(row => row.userTags && userTags.some(tag => row.userTags.includes(tag)));
|
filtered = filtered.filter(row => row.userTags && userTags.some(tag => row.userTags.includes(tag)));
|
||||||
}
|
}
|
||||||
if (product) {
|
if (product) {
|
||||||
filtered = filtered.filter(row => row.product === product);
|
filtered = filtered.filter(row => row.product === product);
|
||||||
}
|
}
|
||||||
|
console.log('[RAG] Отфильтрованные результаты:', filtered);
|
||||||
|
|
||||||
// Берём лучший результат
|
// Берём лучший результат с учётом порога
|
||||||
const best = filtered[0];
|
const best = filtered.find(row => row.score >= threshold);
|
||||||
|
console.log(`[RAG] Выбранный ответ (порог ${threshold}):`, best);
|
||||||
|
|
||||||
// Формируем ответ
|
// Формируем ответ
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -424,9 +424,16 @@ function clearSettingsCache() {
|
|||||||
telegramSettingsCache = null;
|
telegramSettingsCache = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAllBots() {
|
||||||
|
const { rows } = await db.getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id');
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
getTelegramSettings,
|
||||||
getBot,
|
getBot,
|
||||||
stopBot,
|
stopBot,
|
||||||
initTelegramAuth,
|
initTelegramAuth,
|
||||||
clearSettingsCache,
|
clearSettingsCache,
|
||||||
|
getAllBots,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -110,10 +110,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/json-schema" "^7.0.15"
|
"@types/json-schema" "^7.0.15"
|
||||||
|
|
||||||
"@eslint/core@^0.15.0":
|
"@eslint/core@^0.15.1":
|
||||||
version "0.15.0"
|
version "0.15.1"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.0.tgz#8fc04709a7b9a179d9f7d93068fc000cb8c5603d"
|
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.1.tgz#d530d44209cbfe2f82ef86d6ba08760196dd3b60"
|
||||||
integrity sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==
|
integrity sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/json-schema" "^7.0.15"
|
"@types/json-schema" "^7.0.15"
|
||||||
|
|
||||||
@@ -143,11 +143,11 @@
|
|||||||
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
|
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
|
||||||
|
|
||||||
"@eslint/plugin-kit@^0.3.1":
|
"@eslint/plugin-kit@^0.3.1":
|
||||||
version "0.3.2"
|
version "0.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz#0cad96b134d23a653348e3342f485636b5ef4732"
|
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz#32926b59bd407d58d817941e48b2a7049359b1fd"
|
||||||
integrity sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==
|
integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint/core" "^0.15.0"
|
"@eslint/core" "^0.15.1"
|
||||||
levn "^0.4.1"
|
levn "^0.4.1"
|
||||||
|
|
||||||
"@ethereumjs/rlp@^4.0.1":
|
"@ethereumjs/rlp@^4.0.1":
|
||||||
@@ -367,9 +367,9 @@
|
|||||||
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
|
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
|
||||||
|
|
||||||
"@google/genai@^1.0.1":
|
"@google/genai@^1.0.1":
|
||||||
version "1.6.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.6.0.tgz#7a14d505faebe17957b272476debd574d2eae1e0"
|
resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.7.0.tgz#4af2fe86343e990eebfb4adcd0fa744cb0e6907e"
|
||||||
integrity sha512-0vn8wMGesjiEsHeFsl10T8+SFqLj7q+RSE6mml66sE+jwI7U9wW2LQ3qYtwUEaI+P8ZYeEYE5IpYmNLcRQUBPQ==
|
integrity sha512-s/OZLkrIfBwc+SFFaZoKdEogkw4in0YRTGc4Q483jnfchNBWzrNe560eZEfGJHQRPn6YfzJgECCx0sqEOMWvYw==
|
||||||
dependencies:
|
dependencies:
|
||||||
google-auth-library "^9.14.2"
|
google-auth-library "^9.14.2"
|
||||||
ws "^8.18.0"
|
ws "^8.18.0"
|
||||||
@@ -6056,9 +6056,9 @@ simple-update-notifier@^2.0.0:
|
|||||||
semver "^7.5.3"
|
semver "^7.5.3"
|
||||||
|
|
||||||
simple-wcswidth@^1.0.1:
|
simple-wcswidth@^1.0.1:
|
||||||
version "1.1.1"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.1.tgz#a96ff1b5cff660262ea33850e19a0e7249caed50"
|
resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz#66722f37629d5203f9b47c5477b1225b85d6525b"
|
||||||
integrity sha512-R3q3/eoeNBp24CNTASEUrffXi0j9TwPIEvSStlvSrsFimM17sV5EHcMOc86j3K+UWZyLYvH0hRmYGCpCoaJ4vw==
|
integrity sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==
|
||||||
|
|
||||||
sisteransi@^1.0.5:
|
sisteransi@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
@@ -7219,9 +7219,9 @@ zip-stream@^6.0.1:
|
|||||||
readable-stream "^4.0.0"
|
readable-stream "^4.0.0"
|
||||||
|
|
||||||
zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4:
|
zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4:
|
||||||
version "3.24.5"
|
version "3.24.6"
|
||||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3"
|
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d"
|
||||||
integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
|
integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==
|
||||||
|
|
||||||
zod@^3.22.4, zod@^3.25.32:
|
zod@^3.22.4, zod@^3.25.32:
|
||||||
version "3.25.67"
|
version "3.25.67"
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ services:
|
|||||||
condition: service_started
|
condition: service_started
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- backend_node_modules:/app/node_modules
|
|
||||||
- ./frontend/dist:/app/frontend_dist:ro
|
- ./frontend/dist:/app/frontend_dist:ro
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=${NODE_ENV:-development}
|
- NODE_ENV=${NODE_ENV:-development}
|
||||||
@@ -60,6 +59,8 @@ services:
|
|||||||
- FRONTEND_URL=http://localhost:5173
|
- FRONTEND_URL=http://localhost:5173
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
# command: sh -c "yarn run dev" # Временно комментируем эту строку
|
# command: sh -c "yarn run dev" # Временно комментируем эту строку
|
||||||
# command: nodemon server.js # Запускаем через nodemon
|
# command: nodemon server.js # Запускаем через nodemon
|
||||||
|
|
||||||
|
|||||||
@@ -129,11 +129,16 @@ function closeAddColModal() {
|
|||||||
async function handleAddColumn() {
|
async function handleAddColumn() {
|
||||||
if (!newColName.value) return;
|
if (!newColName.value) return;
|
||||||
const data = { name: newColName.value, type: newColType.value };
|
const data = { name: newColName.value, type: newColType.value };
|
||||||
|
const options = {};
|
||||||
if (newColType.value === 'tags') {
|
if (newColType.value === 'tags') {
|
||||||
data.tagIds = selectedTagIds.value;
|
data.tagIds = selectedTagIds.value;
|
||||||
|
options.tagIds = selectedTagIds.value;
|
||||||
}
|
}
|
||||||
if (newColPurpose.value) {
|
if (newColPurpose.value) {
|
||||||
data.purpose = newColPurpose.value;
|
options.purpose = newColPurpose.value;
|
||||||
|
}
|
||||||
|
if (Object.keys(options).length > 0) {
|
||||||
|
data.options = options;
|
||||||
}
|
}
|
||||||
await tablesService.addColumn(props.tableId, data);
|
await tablesService.addColumn(props.tableId, data);
|
||||||
closeAddColModal();
|
closeAddColModal();
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ const routes = [
|
|||||||
component: () => import('../views/SettingsView.vue'),
|
component: () => import('../views/SettingsView.vue'),
|
||||||
// Добавляем дочерние маршруты
|
// Добавляем дочерние маршруты
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'settings-index',
|
||||||
|
component: () => import('@/views/settings/SettingsIndexView.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'ai',
|
path: 'ai',
|
||||||
name: 'settings-ai',
|
name: 'settings-ai',
|
||||||
@@ -50,21 +55,35 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: 'telegram',
|
path: 'telegram',
|
||||||
name: 'settings-telegram',
|
name: 'settings-telegram',
|
||||||
component: () => import('../views/settings/TelegramSettingsView.vue'),
|
component: () => import('../views/settings/AI/TelegramSettingsView.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'email',
|
path: 'email',
|
||||||
name: 'settings-email',
|
name: 'settings-email',
|
||||||
component: () => import('../views/settings/EmailSettingsView.vue'),
|
component: () => import('../views/settings/AI/EmailSettingsView.vue'),
|
||||||
},
|
},
|
||||||
// Опционально: перенаправление со /settings на первую подстраницу
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'settings-index',
|
|
||||||
redirect: { name: 'settings-ai' }
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/ai/openai',
|
||||||
|
name: 'openai-settings',
|
||||||
|
component: () => import('@/views/settings/AI/OpenAISettingsView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/ai/ollama',
|
||||||
|
name: 'ollama-settings',
|
||||||
|
component: () => import('@/views/settings/AI/OllamaSettingsView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/ai/database',
|
||||||
|
name: 'database-settings',
|
||||||
|
component: () => import('@/views/settings/AI/DatabaseSettingsView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/ai/assistant',
|
||||||
|
name: 'ai-assistant-settings',
|
||||||
|
component: () => import('@/views/settings/AI/AiAssistantSettings.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/tables',
|
path: '/tables',
|
||||||
name: 'tables-list',
|
name: 'tables-list',
|
||||||
@@ -120,6 +139,16 @@ const routes = [
|
|||||||
name: 'dle-management',
|
name: 'dle-management',
|
||||||
component: () => import('../views/DleManagementView.vue')
|
component: () => import('../views/DleManagementView.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/ai/telegram',
|
||||||
|
name: 'telegram-settings',
|
||||||
|
component: () => import('@/views/settings/AI/TelegramSettingsView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/ai/email',
|
||||||
|
name: 'email-settings',
|
||||||
|
component: () => import('@/views/settings/AI/EmailSettingsView.vue'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -8,23 +8,12 @@
|
|||||||
>
|
>
|
||||||
<div class="settings-view-container">
|
<div class="settings-view-container">
|
||||||
<h1>Настройки</h1>
|
<h1>Настройки</h1>
|
||||||
|
|
||||||
<div v-if="isLoading">Загрузка данных пользователя...</div>
|
<div v-if="isLoading">Загрузка данных пользователя...</div>
|
||||||
<div v-else-if="!auth.isAuthenticated.value">
|
<div v-else-if="!auth.isAuthenticated.value">
|
||||||
<p>Для доступа к настройкам необходимо <button @click="goToHomeAndShowSidebar">войти</button>.</p>
|
<p>Для доступа к настройкам необходимо <button @click="goToHomeAndShowSidebar">войти</button>.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="settings-navigation-buttons">
|
|
||||||
<div class="buttons-grid">
|
|
||||||
<router-link :to="{ name: 'settings-ai' }" class="btn btn-secondary">ИИ</router-link>
|
|
||||||
<router-link :to="{ name: 'settings-blockchain' }" class="btn btn-secondary">Блокчейн</router-link>
|
|
||||||
<router-link :to="{ name: 'settings-security' }" class="btn btn-secondary">Безопасность</router-link>
|
|
||||||
<router-link :to="{ name: 'settings-interface' }" class="btn btn-secondary">Интерфейс</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Router view для отображения дочерних компонентов настроек -->
|
<!-- Router view для отображения дочерних компонентов настроек -->
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
@@ -32,7 +21,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onBeforeUnmount, computed, defineProps, defineEmits } from 'vue';
|
import { ref, onMounted, watch, onBeforeUnmount, computed, defineProps, defineEmits } from 'vue';
|
||||||
import { useAuthContext } from '../composables/useAuth';
|
import { useAuthContext } from '../composables/useAuth';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { getFromStorage, setToStorage } from '../utils/storage';
|
import { getFromStorage, setToStorage } from '../utils/storage';
|
||||||
import BaseLayout from '../components/BaseLayout.vue';
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
import eventBus from '../utils/eventBus';
|
import eventBus from '../utils/eventBus';
|
||||||
@@ -50,6 +39,7 @@ const emit = defineEmits(['auth-action-completed']);
|
|||||||
|
|
||||||
const auth = useAuthContext();
|
const auth = useAuthContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
|
|
||||||
// Обработчик события изменения авторизации
|
// Обработчик события изменения авторизации
|
||||||
@@ -130,23 +120,7 @@ strong {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Новые стили для кнопок навигации */
|
/* Новые стили для кнопок навигации */
|
||||||
.settings-navigation-buttons {
|
/* Удалено: .settings-navigation-buttons, .buttons-grid, .buttons-grid .btn */
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
padding-top: var(--spacing-lg);
|
|
||||||
border-top: 1px solid var(--color-grey-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons-grid .btn {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Анимации */
|
/* Анимации */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
|
|||||||
276
frontend/src/views/settings/AI/AiAssistantSettings.vue
Normal file
276
frontend/src/views/settings/AI/AiAssistantSettings.vue
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="ai-assistant-settings-block">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
|
<h2>ИИ-ассистент: интеграция и настройки</h2>
|
||||||
|
<div class="ai-assistant-settings settings-panel">
|
||||||
|
<form @submit.prevent="saveSettings">
|
||||||
|
<label>Системный промт</label>
|
||||||
|
<textarea v-model="settings.system_prompt" rows="3" />
|
||||||
|
<label>LLM-модель</label>
|
||||||
|
<select v-if="llmModels.length" v-model="settings.model">
|
||||||
|
<option v-for="m in llmModels" :key="m.id" :value="m.id">{{ m.id }} ({{ m.provider }})</option>
|
||||||
|
</select>
|
||||||
|
<input v-else v-model="settings.model" placeholder="qwen2.5" />
|
||||||
|
<label>Embedding-модель</label>
|
||||||
|
<select v-if="filteredEmbeddingModels.length" v-model="settings.embedding_model">
|
||||||
|
<option v-for="m in filteredEmbeddingModels" :key="m.id" :value="m.id">{{ m.id }} ({{ m.provider }})</option>
|
||||||
|
</select>
|
||||||
|
<input v-else v-model="settings.embedding_model" placeholder="bge-base-zh" />
|
||||||
|
<label>Выбранные RAG-таблицы</label>
|
||||||
|
<select v-model="settings.selected_rag_tables" :multiple="false">
|
||||||
|
<option v-for="table in ragTables" :key="table.id" :value="table.id">{{ table.name }} (id: {{ table.id }})</option>
|
||||||
|
</select>
|
||||||
|
<label>Набор правил</label>
|
||||||
|
<div class="rules-row">
|
||||||
|
<select v-model="settings.rules_id">
|
||||||
|
<option v-for="rule in rulesList" :key="rule.id" :value="rule.id">
|
||||||
|
{{ rule.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" @click="openRuleEditor()">Создать</button>
|
||||||
|
<button type="button" :disabled="!settings.rules_id" @click="openRuleEditor(settings.rules_id)">Редактировать</button>
|
||||||
|
<button type="button" :disabled="!settings.rules_id" @click="deleteRule(settings.rules_id)">Удалить</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedRule">
|
||||||
|
<p><b>Описание:</b> {{ selectedRule.description }}</p>
|
||||||
|
<pre class="rules-json">{{ JSON.stringify(selectedRule.rules, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
<label>Telegram-бот</label>
|
||||||
|
<select v-model="settings.telegram_settings_id">
|
||||||
|
<option v-for="tg in telegramBots" :key="tg.id" :value="tg.id">
|
||||||
|
{{ tg.bot_username }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label>Email для связи</label>
|
||||||
|
<select v-model="settings.email_settings_id">
|
||||||
|
<option v-for="em in emailList" :key="em.id" :value="em.id">
|
||||||
|
{{ em.from_email }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit">Сохранить</button>
|
||||||
|
<button type="button" @click="goBack">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<RuleEditor v-if="showRuleEditor" :rule="editingRule" @close="onRuleEditorClose" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import BaseLayout from '@/components/BaseLayout.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ref, onMounted, computed, watch } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import RuleEditor from '@/components/ai-assistant/RuleEditor.vue';
|
||||||
|
const router = useRouter();
|
||||||
|
const goBack = () => router.push('/settings/ai');
|
||||||
|
const settings = ref({ system_prompt: '', model: '', selected_rag_tables: [], rules_id: null });
|
||||||
|
const userTables = ref([]);
|
||||||
|
const ragTables = computed(() => userTables.value.filter(t => t.is_rag_source_id === 1));
|
||||||
|
const rulesList = ref([]);
|
||||||
|
const showRuleEditor = ref(false);
|
||||||
|
const editingRule = ref(null);
|
||||||
|
const telegramBots = ref([]);
|
||||||
|
const emailList = ref([]);
|
||||||
|
const llmModels = ref([]);
|
||||||
|
const embeddingModels = ref([]);
|
||||||
|
const selectedRule = computed(() => rulesList.value.find(r => r.id === settings.value.rules_id) || null);
|
||||||
|
const selectedLLM = computed(() => llmModels.value.find(m => m.id === settings.value.model));
|
||||||
|
const filteredEmbeddingModels = computed(() => {
|
||||||
|
if (!selectedLLM.value) return embeddingModels.value;
|
||||||
|
return embeddingModels.value.filter(m => m.provider === selectedLLM.value.provider);
|
||||||
|
});
|
||||||
|
async function loadUserTables() {
|
||||||
|
const { data } = await axios.get('/api/tables');
|
||||||
|
userTables.value = Array.isArray(data) ? data : [];
|
||||||
|
}
|
||||||
|
async function loadRules() {
|
||||||
|
const { data } = await axios.get('/api/settings/ai-assistant-rules');
|
||||||
|
rulesList.value = data.rules || [];
|
||||||
|
}
|
||||||
|
async function loadSettings() {
|
||||||
|
const { data } = await axios.get('/api/settings/ai-assistant');
|
||||||
|
if (data.success && data.settings) {
|
||||||
|
settings.value = data.settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function loadTelegramBots() {
|
||||||
|
const { data } = await axios.get('/api/settings/telegram-settings/list');
|
||||||
|
telegramBots.value = data.items || [];
|
||||||
|
}
|
||||||
|
async function loadEmailList() {
|
||||||
|
const { data } = await axios.get('/api/settings/email-settings/list');
|
||||||
|
emailList.value = data.items || [];
|
||||||
|
}
|
||||||
|
async function loadLLMModels() {
|
||||||
|
const { data } = await axios.get('/api/settings/llm-models');
|
||||||
|
llmModels.value = data.models || [];
|
||||||
|
}
|
||||||
|
async function loadEmbeddingModels() {
|
||||||
|
const { data } = await axios.get('/api/settings/embedding-models');
|
||||||
|
embeddingModels.value = data.models || [];
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
loadSettings();
|
||||||
|
loadUserTables();
|
||||||
|
loadRules();
|
||||||
|
loadTelegramBots();
|
||||||
|
loadEmailList();
|
||||||
|
loadLLMModels();
|
||||||
|
loadEmbeddingModels();
|
||||||
|
});
|
||||||
|
async function saveSettings() {
|
||||||
|
await axios.put('/api/settings/ai-assistant', settings.value);
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
function openRuleEditor(ruleId = null) {
|
||||||
|
if (ruleId) {
|
||||||
|
editingRule.value = rulesList.value.find(r => r.id === ruleId) || null;
|
||||||
|
} else {
|
||||||
|
editingRule.value = null;
|
||||||
|
}
|
||||||
|
showRuleEditor.value = true;
|
||||||
|
}
|
||||||
|
async function deleteRule(ruleId) {
|
||||||
|
if (!confirm('Удалить этот набор правил?')) return;
|
||||||
|
await axios.delete(`/api/settings/ai-assistant-rules/${ruleId}`);
|
||||||
|
await loadRules();
|
||||||
|
if (settings.value.rules_id === ruleId) settings.value.rules_id = null;
|
||||||
|
}
|
||||||
|
async function onRuleEditorClose(updated) {
|
||||||
|
showRuleEditor.value = false;
|
||||||
|
editingRule.value = null;
|
||||||
|
if (updated) await loadRules();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ai-assistant-settings-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100vw;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.ai-assistant-settings.settings-panel {
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
textarea, input, select {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
select[multiple] {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
.rules-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.rules-json {
|
||||||
|
background: #f7f7f7;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.95em;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
button[type="submit"], .actions button {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
button[type="button"] {
|
||||||
|
background: #eee;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.modal-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.25);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 16px rgba(0,0,0,0.12);
|
||||||
|
padding: 2rem;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #c00;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.rag-table-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
}
|
||||||
|
.rag-table-link {
|
||||||
|
color: #2ecc40;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.rag-table-link:hover {
|
||||||
|
color: #27ae38;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
210
frontend/src/views/settings/AI/DatabaseSettingsView.vue
Normal file
210
frontend/src/views/settings/AI/DatabaseSettingsView.vue
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="db-settings-block">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
|
<h2>База данных: интеграция и настройки</h2>
|
||||||
|
<div class="db-settings settings-panel">
|
||||||
|
<form v-if="editMode" @submit.prevent="saveDbSettings" class="settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dbHost">Host</label>
|
||||||
|
<input id="dbHost" v-model="form.dbHost" type="text" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dbPort">Port</label>
|
||||||
|
<input id="dbPort" v-model.number="form.dbPort" type="number" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dbName">Database</label>
|
||||||
|
<input id="dbName" v-model="form.dbName" type="text" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dbUser">User</label>
|
||||||
|
<input id="dbUser" v-model="form.dbUser" type="text" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dbPassword">Password</label>
|
||||||
|
<input id="dbPassword" v-model="form.dbPassword" type="password" :placeholder="form.dbPassword ? 'Изменить пароль' : 'Введите пароль'" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="save-btn">Сохранить</button>
|
||||||
|
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||||
|
</form>
|
||||||
|
<div v-else class="settings-view">
|
||||||
|
<div class="view-row"><span>Host:</span> <b>{{ form.dbHost }}</b></div>
|
||||||
|
<div class="view-row"><span>Port:</span> <b>{{ form.dbPort }}</b></div>
|
||||||
|
<div class="view-row"><span>Database:</span> <b>{{ form.dbName }}</b></div>
|
||||||
|
<div class="view-row"><span>User:</span> <b>{{ form.dbUser }}</b></div>
|
||||||
|
<div class="view-row"><span>Password:</span> <b>••••••••••••••••••••••••••••••••</b></div>
|
||||||
|
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||||
|
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BaseLayout from '@/components/BaseLayout.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { reactive, ref, onMounted } from 'vue';
|
||||||
|
import api from '@/api/axios';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const goBack = () => router.push('/settings/ai');
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
dbHost: '',
|
||||||
|
dbPort: 5432,
|
||||||
|
dbName: '',
|
||||||
|
dbUser: '',
|
||||||
|
dbPassword: ''
|
||||||
|
});
|
||||||
|
const original = reactive({});
|
||||||
|
const editMode = ref(false);
|
||||||
|
|
||||||
|
const loadDbSettings = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/api/settings/db-settings');
|
||||||
|
if (res.data.success) {
|
||||||
|
const s = res.data.settings;
|
||||||
|
form.dbHost = s.db_host;
|
||||||
|
form.dbPort = s.db_port;
|
||||||
|
form.dbName = s.db_name;
|
||||||
|
form.dbUser = s.db_user;
|
||||||
|
form.dbPassword = '';
|
||||||
|
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// обработка ошибки
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadDbSettings();
|
||||||
|
editMode.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveDbSettings = async () => {
|
||||||
|
try {
|
||||||
|
await api.put('/api/settings/db-settings', {
|
||||||
|
db_host: form.dbHost,
|
||||||
|
db_port: form.dbPort,
|
||||||
|
db_name: form.dbName,
|
||||||
|
db_user: form.dbUser,
|
||||||
|
db_password: form.dbPassword || undefined
|
||||||
|
});
|
||||||
|
alert('Настройки базы данных сохранены');
|
||||||
|
form.dbPassword = '';
|
||||||
|
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||||
|
editMode.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка сохранения настроек базы данных');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||||
|
form.dbPassword = '';
|
||||||
|
editMode.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.db-settings-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto 0 auto;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.db-settings.settings-panel {
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.settings-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.save-btn {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.save-btn:hover {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
.cancel-btn {
|
||||||
|
background: #eee;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
.settings-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
.view-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
.edit-btn {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.edit-btn:hover {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
225
frontend/src/views/settings/AI/EmailSettingsView.vue
Normal file
225
frontend/src/views/settings/AI/EmailSettingsView.vue
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="email-settings-block">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
|
<h2>Email: интеграция и настройки</h2>
|
||||||
|
<div class="email-settings settings-panel">
|
||||||
|
<form v-if="editMode" @submit.prevent="saveEmailSettings" class="settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="smtpHost">SMTP Host</label>
|
||||||
|
<input id="smtpHost" v-model="form.smtpHost" type="text" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="smtpPort">SMTP Port</label>
|
||||||
|
<input id="smtpPort" v-model.number="form.smtpPort" type="number" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="smtpUser">SMTP User</label>
|
||||||
|
<input id="smtpUser" v-model="form.smtpUser" type="text" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="smtpPassword">SMTP Password</label>
|
||||||
|
<input id="smtpPassword" v-model="form.smtpPassword" type="password" :placeholder="form.smtpPassword ? 'Изменить пароль' : 'Введите пароль'" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="imapHost">IMAP Host</label>
|
||||||
|
<input id="imapHost" v-model="form.imapHost" type="text" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="imapPort">IMAP Port</label>
|
||||||
|
<input id="imapPort" v-model.number="form.imapPort" type="number" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fromEmail">From Email</label>
|
||||||
|
<input id="fromEmail" v-model="form.fromEmail" type="email" required />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="save-btn">Сохранить</button>
|
||||||
|
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||||
|
</form>
|
||||||
|
<div v-else class="settings-view">
|
||||||
|
<div class="view-row"><span>SMTP Host:</span> <b>{{ form.smtpHost }}</b></div>
|
||||||
|
<div class="view-row"><span>SMTP Port:</span> <b>{{ form.smtpPort }}</b></div>
|
||||||
|
<div class="view-row"><span>SMTP User:</span> <b>{{ form.smtpUser }}</b></div>
|
||||||
|
<div class="view-row"><span>IMAP Host:</span> <b>{{ form.imapHost }}</b></div>
|
||||||
|
<div class="view-row"><span>IMAP Port:</span> <b>{{ form.imapPort }}</b></div>
|
||||||
|
<div class="view-row"><span>From Email:</span> <b>{{ form.fromEmail }}</b></div>
|
||||||
|
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||||
|
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BaseLayout from '@/components/BaseLayout.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { reactive, ref, onMounted } from 'vue';
|
||||||
|
import api from '@/api/axios';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const goBack = () => router.push('/settings/ai');
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
smtpHost: '',
|
||||||
|
smtpPort: 465,
|
||||||
|
smtpUser: '',
|
||||||
|
smtpPassword: '',
|
||||||
|
imapHost: '',
|
||||||
|
imapPort: 993,
|
||||||
|
fromEmail: ''
|
||||||
|
});
|
||||||
|
const original = reactive({});
|
||||||
|
const editMode = ref(false);
|
||||||
|
|
||||||
|
const loadEmailSettings = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/api/settings/email-settings');
|
||||||
|
if (res.data.success) {
|
||||||
|
const s = res.data.settings;
|
||||||
|
form.smtpHost = s.smtp_host;
|
||||||
|
form.smtpPort = s.smtp_port;
|
||||||
|
form.smtpUser = s.smtp_user;
|
||||||
|
form.imapHost = s.imap_host || '';
|
||||||
|
form.imapPort = s.imap_port || 993;
|
||||||
|
form.fromEmail = s.from_email;
|
||||||
|
form.smtpPassword = '';
|
||||||
|
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// обработка ошибки
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadEmailSettings();
|
||||||
|
editMode.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveEmailSettings = async () => {
|
||||||
|
try {
|
||||||
|
await api.put('/api/settings/email-settings', {
|
||||||
|
smtp_host: form.smtpHost,
|
||||||
|
smtp_port: form.smtpPort,
|
||||||
|
smtp_user: form.smtpUser,
|
||||||
|
smtp_password: form.smtpPassword || undefined,
|
||||||
|
imap_host: form.imapHost,
|
||||||
|
imap_port: form.imapPort,
|
||||||
|
from_email: form.fromEmail
|
||||||
|
});
|
||||||
|
alert('Настройки Email сохранены');
|
||||||
|
form.smtpPassword = '';
|
||||||
|
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||||
|
editMode.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка сохранения email-настроек');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||||
|
form.smtpPassword = '';
|
||||||
|
editMode.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.email-settings-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto 0 auto;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.email-settings.settings-panel {
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.settings-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.save-btn {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.save-btn:hover {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
.cancel-btn {
|
||||||
|
background: #eee;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
.settings-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
.view-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
.edit-btn {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.edit-btn:hover {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
frontend/src/views/settings/AI/OllamaSettingsView.vue
Normal file
65
frontend/src/views/settings/AI/OllamaSettingsView.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="ollama-settings-block">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
|
<h2>Ollama: интеграция и настройки</h2>
|
||||||
|
<AIProviderSettings
|
||||||
|
provider="ollama"
|
||||||
|
label="Ollama (локальные модели)"
|
||||||
|
description="Настройка Ollama для локальных open-source моделей. Ключ не требуется."
|
||||||
|
apiKeyPlaceholder=""
|
||||||
|
baseUrlPlaceholder="http://localhost:11434"
|
||||||
|
:showApiKey="false"
|
||||||
|
:showBaseUrl="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BaseLayout from '@/components/BaseLayout.vue';
|
||||||
|
import AIProviderSettings from '@/views/settings/AIProviderSettings.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const goBack = () => router.push('/settings/ai');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ollama-settings-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto 0 auto;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.ai-provider-settings.settings-panel {
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
frontend/src/views/settings/AI/OpenAISettingsView.vue
Normal file
65
frontend/src/views/settings/AI/OpenAISettingsView.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="openai-settings-block">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
|
<h2>OpenAI: интеграция и настройки</h2>
|
||||||
|
<AIProviderSettings
|
||||||
|
provider="openai"
|
||||||
|
label="OpenAI API Key"
|
||||||
|
description="Введите OpenAI API Key и (опционально) Base URL для кастомных endpoint."
|
||||||
|
apiKeyPlaceholder="sk-..."
|
||||||
|
baseUrlPlaceholder="https://api.openai.com/v1"
|
||||||
|
:showApiKey="true"
|
||||||
|
:showBaseUrl="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BaseLayout from '@/components/BaseLayout.vue';
|
||||||
|
import AIProviderSettings from '@/views/settings/AIProviderSettings.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const goBack = () => router.push('/settings/ai');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.openai-settings-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto 0 auto;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.ai-provider-settings.settings-panel {
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
186
frontend/src/views/settings/AI/TelegramSettingsView.vue
Normal file
186
frontend/src/views/settings/AI/TelegramSettingsView.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="telegram-settings-block">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
|
<h2>Telegram: интеграция и настройки</h2>
|
||||||
|
<div class="telegram-settings settings-panel">
|
||||||
|
<form v-if="editMode" @submit.prevent="saveTelegramSettings" class="settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="botToken">Bot Token</label>
|
||||||
|
<input id="botToken" v-model="form.botToken" type="text" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="botUsername">Bot Username</label>
|
||||||
|
<input id="botUsername" v-model="form.botUsername" type="text" required />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="save-btn">Сохранить</button>
|
||||||
|
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||||
|
</form>
|
||||||
|
<div v-else class="settings-view">
|
||||||
|
<div class="view-row"><span>Bot Token:</span> <b>••••••••••••••••••••••••••••••••••••••••</b></div>
|
||||||
|
<div class="view-row"><span>Bot Username:</span> <b>{{ form.botUsername }}</b></div>
|
||||||
|
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||||
|
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BaseLayout from '@/components/BaseLayout.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { reactive, ref, onMounted } from 'vue';
|
||||||
|
import api from '@/api/axios';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const goBack = () => router.push('/settings/ai');
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
botToken: '',
|
||||||
|
botUsername: ''
|
||||||
|
});
|
||||||
|
const original = reactive({});
|
||||||
|
const editMode = ref(false);
|
||||||
|
|
||||||
|
const loadTelegramSettings = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/api/settings/telegram-settings');
|
||||||
|
if (res.data.success) {
|
||||||
|
const s = res.data.settings;
|
||||||
|
form.botToken = s.bot_token || '';
|
||||||
|
form.botUsername = s.bot_username;
|
||||||
|
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// обработка ошибки
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadTelegramSettings();
|
||||||
|
editMode.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveTelegramSettings = async () => {
|
||||||
|
try {
|
||||||
|
await api.put('/api/telegram-settings', {
|
||||||
|
bot_token: form.botToken,
|
||||||
|
bot_username: form.botUsername
|
||||||
|
});
|
||||||
|
alert('Настройки Telegram сохранены');
|
||||||
|
form.botToken = '';
|
||||||
|
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||||
|
editMode.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка сохранения telegram-настроек');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||||
|
form.botToken = '';
|
||||||
|
editMode.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.telegram-settings-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto 0 auto;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.telegram-settings.settings-panel {
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.settings-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.save-btn {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.save-btn:hover {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
.cancel-btn {
|
||||||
|
background: #eee;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
.settings-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
.view-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
.edit-btn {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.edit-btn:hover {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,10 +15,18 @@
|
|||||||
<input type="text" v-model="baseUrl" :placeholder="baseUrlPlaceholder" />
|
<input type="text" v-model="baseUrl" :placeholder="baseUrlPlaceholder" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="models.length">
|
<div v-if="models.length">
|
||||||
<label>Модель:</label>
|
<label>Модель (LLM):</label>
|
||||||
<select v-model="selectedModel">
|
<select v-model="selectedModel">
|
||||||
<option v-for="model in models" :key="model.id || model" :value="model.id || model">
|
<option v-for="model in models" :key="model.id || model.name || model" :value="model.id || model.name || model">
|
||||||
{{ model.id || model }}
|
{{ model.id || model.name || model }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-if="embeddingModels.length">
|
||||||
|
<label>Embeddings-модель:</label>
|
||||||
|
<select v-model="selectedEmbeddingModel">
|
||||||
|
<option v-for="model in embeddingModels" :key="model.id || model.name || model" :value="model.id || model.name || model">
|
||||||
|
{{ model.id || model.name || model }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +57,9 @@ const props = defineProps({
|
|||||||
const apiKey = ref('');
|
const apiKey = ref('');
|
||||||
const baseUrl = ref('');
|
const baseUrl = ref('');
|
||||||
const selectedModel = ref('');
|
const selectedModel = ref('');
|
||||||
|
const selectedEmbeddingModel = ref('');
|
||||||
const models = ref([]);
|
const models = ref([]);
|
||||||
|
const embeddingModels = ref([]);
|
||||||
const hasSettings = ref(false);
|
const hasSettings = ref(false);
|
||||||
const verifying = ref(false);
|
const verifying = ref(false);
|
||||||
const verifyStatus = ref(null);
|
const verifyStatus = ref(null);
|
||||||
@@ -65,9 +75,11 @@ async function loadSettings() {
|
|||||||
apiKey.value = data.settings.api_key || '';
|
apiKey.value = data.settings.api_key || '';
|
||||||
baseUrl.value = data.settings.base_url || '';
|
baseUrl.value = data.settings.base_url || '';
|
||||||
selectedModel.value = data.settings.selected_model || '';
|
selectedModel.value = data.settings.selected_model || '';
|
||||||
|
selectedEmbeddingModel.value = data.settings.embedding_model || '';
|
||||||
hasSettings.value = true;
|
hasSettings.value = true;
|
||||||
if (apiKey.value || props.provider === 'ollama') {
|
if (apiKey.value || props.provider === 'ollama') {
|
||||||
await loadModels();
|
await loadModels();
|
||||||
|
await loadEmbeddingModels();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hasSettings.value = false;
|
hasSettings.value = false;
|
||||||
@@ -82,13 +94,30 @@ async function loadModels() {
|
|||||||
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
|
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
|
||||||
models.value = data.models || [];
|
models.value = data.models || [];
|
||||||
if (!selectedModel.value && models.value.length) {
|
if (!selectedModel.value && models.value.length) {
|
||||||
selectedModel.value = models.value[0].id || models.value[0];
|
const first = models.value[0];
|
||||||
|
selectedModel.value = first.id || first.name || first;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
models.value = [];
|
models.value = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadEmbeddingModels() {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
|
||||||
|
embeddingModels.value = (data.models || []).filter(m => {
|
||||||
|
const name = m.id || m.name || m;
|
||||||
|
return name && name.toLowerCase().includes('embed');
|
||||||
|
});
|
||||||
|
if (!selectedEmbeddingModel.value && embeddingModels.value.length) {
|
||||||
|
const first = embeddingModels.value[0];
|
||||||
|
selectedEmbeddingModel.value = first.id || first.name || first;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
embeddingModels.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onVerify() {
|
async function onVerify() {
|
||||||
verifying.value = true;
|
verifying.value = true;
|
||||||
verifyStatus.value = null;
|
verifyStatus.value = null;
|
||||||
@@ -119,6 +148,7 @@ async function onSave() {
|
|||||||
api_key: apiKey.value,
|
api_key: apiKey.value,
|
||||||
base_url: baseUrl.value,
|
base_url: baseUrl.value,
|
||||||
selected_model: selectedModel.value,
|
selected_model: selectedModel.value,
|
||||||
|
embedding_model: selectedEmbeddingModel.value,
|
||||||
});
|
});
|
||||||
saveStatus.value = true;
|
saveStatus.value = true;
|
||||||
hasSettings.value = true;
|
hasSettings.value = true;
|
||||||
@@ -135,7 +165,9 @@ async function onDelete() {
|
|||||||
apiKey.value = '';
|
apiKey.value = '';
|
||||||
baseUrl.value = '';
|
baseUrl.value = '';
|
||||||
selectedModel.value = '';
|
selectedModel.value = '';
|
||||||
|
selectedEmbeddingModel.value = '';
|
||||||
models.value = [];
|
models.value = [];
|
||||||
|
embeddingModels.value = [];
|
||||||
hasSettings.value = false;
|
hasSettings.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,8 +183,9 @@ watch([apiKey, baseUrl], () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.ai-provider-settings.settings-panel {
|
.ai-provider-settings.settings-panel {
|
||||||
padding: var(--block-padding);
|
padding: var(--block-padding);
|
||||||
background-color: var(--color-light);
|
/* background-color: var(--color-light); */
|
||||||
border-radius: var(--radius-md);
|
/* border-radius: var(--radius-md); */
|
||||||
|
/* box-shadow: 0 2px 8px rgba(0,0,0,0.08); */
|
||||||
margin-top: var(--spacing-lg);
|
margin-top: var(--spacing-lg);
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="ai-assistant-settings-modal">
|
|
||||||
<h2>Настройки ИИ-ассистента</h2>
|
|
||||||
<form @submit.prevent="saveSettings">
|
|
||||||
<label>Системный промт</label>
|
|
||||||
<textarea v-model="settings.system_prompt" rows="3" />
|
|
||||||
<label>Языки</label>
|
|
||||||
<input v-model="languagesInput" placeholder="ru, en, es" />
|
|
||||||
<label>Модель</label>
|
|
||||||
<input v-model="settings.model" placeholder="qwen2.5" />
|
|
||||||
<label>Выбранные RAG-таблицы</label>
|
|
||||||
<ul class="rag-table-list">
|
|
||||||
<li v-for="table in ragTables" :key="table.id">
|
|
||||||
<router-link :to="{ name: 'user-table-view', params: { id: table.id } }" class="rag-table-link">
|
|
||||||
{{ table.name }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<label>Набор правил</label>
|
|
||||||
<div class="rules-row">
|
|
||||||
<select v-model="settings.rules_id">
|
|
||||||
<option v-for="rule in rulesList" :key="rule.id" :value="rule.id">
|
|
||||||
{{ rule.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<button type="button" @click="openRuleEditor()">Создать</button>
|
|
||||||
<button type="button" :disabled="!settings.rules_id" @click="openRuleEditor(settings.rules_id)">Редактировать</button>
|
|
||||||
<button type="button" :disabled="!settings.rules_id" @click="deleteRule(settings.rules_id)">Удалить</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedRule">
|
|
||||||
<p><b>Описание:</b> {{ selectedRule.description }}</p>
|
|
||||||
<pre class="rules-json">{{ JSON.stringify(selectedRule.rules, null, 2) }}</pre>
|
|
||||||
</div>
|
|
||||||
<label>Telegram-бот</label>
|
|
||||||
<select v-model="settings.telegram_settings_id">
|
|
||||||
<option v-for="tg in telegramBots" :key="tg.id" :value="tg.id">
|
|
||||||
{{ tg.bot_username }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label>Email для связи</label>
|
|
||||||
<select v-model="settings.email_settings_id">
|
|
||||||
<option v-for="em in emailList" :key="em.id" :value="em.id">
|
|
||||||
{{ em.from_email }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<div class="actions">
|
|
||||||
<button type="submit">Сохранить</button>
|
|
||||||
<button type="button" @click="emit('cancel')">Отмена</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<RuleEditor v-if="showRuleEditor" :rule="editingRule" @close="onRuleEditorClose" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, computed } from 'vue';
|
|
||||||
import axios from 'axios';
|
|
||||||
import RuleEditor from '../../components/ai-assistant/RuleEditor.vue';
|
|
||||||
const emit = defineEmits(['cancel']);
|
|
||||||
const settings = ref({ system_prompt: '', model: '', selected_rag_tables: [], languages: [], rules_id: null });
|
|
||||||
const languagesInput = ref('');
|
|
||||||
const userTables = ref([]);
|
|
||||||
const ragTables = computed(() => userTables.value.filter(t => t.is_rag_source_id === 1));
|
|
||||||
const rulesList = ref([]);
|
|
||||||
const showRuleEditor = ref(false);
|
|
||||||
const editingRule = ref(null);
|
|
||||||
const telegramBots = ref([]);
|
|
||||||
const emailList = ref([]);
|
|
||||||
|
|
||||||
const selectedRule = computed(() => rulesList.value.find(r => r.id === settings.value.rules_id) || null);
|
|
||||||
|
|
||||||
async function loadUserTables() {
|
|
||||||
const { data } = await axios.get('/api/tables');
|
|
||||||
userTables.value = Array.isArray(data) ? data : [];
|
|
||||||
}
|
|
||||||
async function loadRules() {
|
|
||||||
const { data } = await axios.get('/api/settings/ai-assistant-rules');
|
|
||||||
rulesList.value = data.rules || [];
|
|
||||||
}
|
|
||||||
async function loadSettings() {
|
|
||||||
const { data } = await axios.get('/api/settings/ai-assistant');
|
|
||||||
if (data.success && data.settings) {
|
|
||||||
settings.value = data.settings;
|
|
||||||
languagesInput.value = (data.settings.languages || []).join(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function loadTelegramBots() {
|
|
||||||
const { data } = await axios.get('/api/settings/telegram-settings');
|
|
||||||
telegramBots.value = data.items || [];
|
|
||||||
}
|
|
||||||
async function loadEmailList() {
|
|
||||||
const { data } = await axios.get('/api/settings/email-settings');
|
|
||||||
emailList.value = data.items || [];
|
|
||||||
}
|
|
||||||
onMounted(() => {
|
|
||||||
loadSettings();
|
|
||||||
loadUserTables();
|
|
||||||
loadRules();
|
|
||||||
loadTelegramBots();
|
|
||||||
loadEmailList();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function saveSettings() {
|
|
||||||
settings.value.languages = languagesInput.value.split(',').map(s => s.trim()).filter(Boolean);
|
|
||||||
await axios.put('/api/settings/ai-assistant', settings.value);
|
|
||||||
emit('cancel');
|
|
||||||
}
|
|
||||||
|
|
||||||
function openRuleEditor(ruleId = null) {
|
|
||||||
if (ruleId) {
|
|
||||||
editingRule.value = rulesList.value.find(r => r.id === ruleId) || null;
|
|
||||||
} else {
|
|
||||||
editingRule.value = null;
|
|
||||||
}
|
|
||||||
showRuleEditor.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteRule(ruleId) {
|
|
||||||
if (!confirm('Удалить этот набор правил?')) return;
|
|
||||||
await axios.delete(`/api/settings/ai-assistant-rules/${ruleId}`);
|
|
||||||
await loadRules();
|
|
||||||
if (settings.value.rules_id === ruleId) settings.value.rules_id = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onRuleEditorClose(updated) {
|
|
||||||
showRuleEditor.value = false;
|
|
||||||
editingRule.value = null;
|
|
||||||
if (updated) await loadRules();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.ai-assistant-settings-modal {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 540px;
|
|
||||||
margin: 2rem auto;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-top: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
textarea, input, select {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
select[multiple] {
|
|
||||||
min-height: 80px;
|
|
||||||
}
|
|
||||||
.rules-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
.rules-json {
|
|
||||||
background: #f7f7f7;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-size: 0.95em;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
button[type="submit"], .actions button {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
button[type="button"] {
|
|
||||||
background: #eee;
|
|
||||||
color: #333;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.modal-bg {
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
|
||||||
background: rgba(0,0,0,0.25);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
.modal {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 2px 16px rgba(0,0,0,0.12);
|
|
||||||
padding: 2rem;
|
|
||||||
min-width: 320px;
|
|
||||||
max-width: 420px;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #c00;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
.rag-table-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
}
|
|
||||||
.rag-table-link {
|
|
||||||
color: #2ecc40;
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.rag-table-link:hover {
|
|
||||||
color: #27ae38;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,46 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ai-settings settings-panel">
|
<div class="ai-settings settings-panel" style="position:relative">
|
||||||
|
<button class="close-btn" @click="$router.push('/settings')">×</button>
|
||||||
<h2>Интеграции</h2>
|
<h2>Интеграции</h2>
|
||||||
<div class="integration-blocks" v-if="!showProvider && !showEmailSettings && !showTelegramSettings && !showDbSettings">
|
<div class="integration-blocks" v-if="!showProvider && !showEmailSettings && !showTelegramSettings && !showDbSettings">
|
||||||
<div class="integration-block">
|
<div class="integration-block">
|
||||||
<h3>OpenAI</h3>
|
<h3>OpenAI</h3>
|
||||||
<p>Интеграция с OpenAI (GPT-4, GPT-3.5 и др.).</p>
|
<p>Интеграция с OpenAI (GPT-4, GPT-3.5 и др.).</p>
|
||||||
<button class="details-btn" @click="showProvider = 'openai'">Подробнее</button>
|
<button class="details-btn" @click="$router.push('/settings/ai/openai')">Подробнее</button>
|
||||||
</div>
|
|
||||||
<div class="integration-block">
|
|
||||||
<h3>Anthropic</h3>
|
|
||||||
<p>Интеграция с Anthropic Claude (Claude 3 и др.).</p>
|
|
||||||
<button class="details-btn" @click="showProvider = 'anthropic'">Подробнее</button>
|
|
||||||
</div>
|
|
||||||
<div class="integration-block">
|
|
||||||
<h3>Google Gemini</h3>
|
|
||||||
<p>Интеграция с Google Gemini (Gemini 1.5, 1.0 и др.).</p>
|
|
||||||
<button class="details-btn" @click="showProvider = 'google'">Подробнее</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="integration-block">
|
<div class="integration-block">
|
||||||
<h3>Ollama</h3>
|
<h3>Ollama</h3>
|
||||||
<p>Локальные open-source модели через Ollama.</p>
|
<p>Локальные open-source модели через Ollama.</p>
|
||||||
<button class="details-btn" @click="showProvider = 'ollama'">Подробнее</button>
|
<button class="details-btn" @click="$router.push('/settings/ai/ollama')">Подробнее</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="integration-block">
|
<div class="integration-block">
|
||||||
<h3>Telegram</h3>
|
<h3>Telegram</h3>
|
||||||
<p>Интеграция с Telegram-ботом для уведомлений и авторизации.</p>
|
<p>Интеграция с Telegram-ботом для уведомлений и авторизации.</p>
|
||||||
<button class="details-btn" @click="showTelegramSettings = true">Подробнее</button>
|
<button class="details-btn" @click="$router.push('/settings/ai/telegram')">Подробнее</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="integration-block">
|
<div class="integration-block">
|
||||||
<h3>Email</h3>
|
<h3>Email</h3>
|
||||||
<p>Интеграция с Email для отправки писем и уведомлений.</p>
|
<p>Интеграция с Email для отправки писем и уведомлений.</p>
|
||||||
<button class="details-btn" @click="showEmailSettings = true">Подробнее</button>
|
<button class="details-btn" @click="$router.push('/settings/ai/email')">Подробнее</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="integration-block">
|
<div class="integration-block">
|
||||||
<h3>База данных</h3>
|
<h3>База данных</h3>
|
||||||
<p>Интеграция с PostgreSQL для хранения данных приложения и управления настройками.</p>
|
<p>Интеграция с PostgreSQL для хранения данных приложения и управления настройками.</p>
|
||||||
<button class="details-btn" @click="showDbSettings = true">Подробнее</button>
|
<button class="details-btn" @click="$router.push('/settings/ai/database')">Подробнее</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="integration-block">
|
<div class="integration-block">
|
||||||
<h3>ИИ-ассистент</h3>
|
<h3>ИИ-ассистент</h3>
|
||||||
<p>Настройки поведения, языков, моделей и правил работы ассистента.</p>
|
<p>Настройки поведения, языков, моделей и правил работы ассистента.</p>
|
||||||
<button class="details-btn" @click="showAiAssistantSettings = true">Подробнее</button>
|
<button class="details-btn" @click="$router.push('/settings/ai/assistant')">Подробнее</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AIProviderSettings
|
<AIProviderSettings
|
||||||
@@ -54,20 +45,12 @@
|
|||||||
:showBaseUrl="providerLabels[showProvider].showBaseUrl"
|
:showBaseUrl="providerLabels[showProvider].showBaseUrl"
|
||||||
@cancel="showProvider = null"
|
@cancel="showProvider = null"
|
||||||
/>
|
/>
|
||||||
<TelegramSettingsView v-if="showTelegramSettings" @cancel="showTelegramSettings = false" />
|
|
||||||
<EmailSettingsView v-if="showEmailSettings" @cancel="showEmailSettings = false" />
|
|
||||||
<DatabaseSettingsView v-if="showDbSettings" @cancel="showDbSettings = false" />
|
|
||||||
<AiAssistantSettings v-if="showAiAssistantSettings" @cancel="showAiAssistantSettings = false" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import AIProviderSettings from './AIProviderSettings.vue';
|
import AIProviderSettings from './AIProviderSettings.vue';
|
||||||
import TelegramSettingsView from './TelegramSettingsView.vue';
|
|
||||||
import EmailSettingsView from './EmailSettingsView.vue';
|
|
||||||
import DatabaseSettingsView from './DatabaseSettingsView.vue';
|
|
||||||
import AiAssistantSettings from './AiAssistantSettings.vue';
|
|
||||||
|
|
||||||
const showProvider = ref(null);
|
const showProvider = ref(null);
|
||||||
const showTelegramSettings = ref(false);
|
const showTelegramSettings = ref(false);
|
||||||
@@ -153,4 +136,19 @@ const providerLabels = {
|
|||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="blockchain-settings settings-panel">
|
<div class="blockchain-settings settings-panel" style="position:relative">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
|
|
||||||
<!-- Панель Создать новое DLE (Digital Legal Entity) -->
|
<!-- Панель Создать новое DLE (Digital Legal Entity) -->
|
||||||
<div class="sub-settings-panel">
|
<div class="sub-settings-panel">
|
||||||
@@ -320,6 +321,7 @@ import axios from 'axios'; // Предполагаем, что axios досту
|
|||||||
import { useAuthContext } from '@/composables/useAuth'; // Импортируем composable useAuth
|
import { useAuthContext } from '@/composables/useAuth'; // Импортируем composable useAuth
|
||||||
import dleService from '@/services/dleService';
|
import dleService from '@/services/dleService';
|
||||||
import useBlockchainNetworks from '@/composables/useBlockchainNetworks'; // Импортируем composable для работы с сетями
|
import useBlockchainNetworks from '@/composables/useBlockchainNetworks'; // Импортируем composable для работы с сетями
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
// TODO: Импортировать API
|
// TODO: Импортировать API
|
||||||
|
|
||||||
const { address, isAdmin, auth, user } = useAuthContext(); // Получаем объект адреса и статус админа
|
const { address, isAdmin, auth, user } = useAuthContext(); // Получаем объект адреса и статус админа
|
||||||
@@ -340,6 +342,8 @@ const {
|
|||||||
loadingNetworks
|
loadingNetworks
|
||||||
} = useBlockchainNetworks();
|
} = useBlockchainNetworks();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// Добавляем настройки безопасности и подключения
|
// Добавляем настройки безопасности и подключения
|
||||||
const securitySettings = reactive({
|
const securitySettings = reactive({
|
||||||
rpcConfigs: [], // Массив для хранения { networkId: string, rpcUrl: string, chainId: number }
|
rpcConfigs: [], // Массив для хранения { networkId: string, rpcUrl: string, chainId: number }
|
||||||
@@ -995,6 +999,8 @@ const testRpcHandler = async (rpc) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goBack = () => router.push('/settings');
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -1287,4 +1293,20 @@ h3 {
|
|||||||
background-color: #a0d2dc;
|
background-color: #a0d2dc;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="db-settings settings-panel">
|
|
||||||
<h2>Настройки базы данных</h2>
|
|
||||||
<form v-if="editMode" @submit.prevent="saveDbSettings" class="settings-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dbHost">Host</label>
|
|
||||||
<input id="dbHost" v-model="form.dbHost" type="text" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dbPort">Port</label>
|
|
||||||
<input id="dbPort" v-model.number="form.dbPort" type="number" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dbName">Database</label>
|
|
||||||
<input id="dbName" v-model="form.dbName" type="text" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dbUser">User</label>
|
|
||||||
<input id="dbUser" v-model="form.dbUser" type="text" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dbPassword">Password</label>
|
|
||||||
<input id="dbPassword" v-model="form.dbPassword" type="password" :placeholder="form.dbPassword ? 'Изменить пароль' : 'Введите пароль'" />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="save-btn">Сохранить</button>
|
|
||||||
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
|
||||||
</form>
|
|
||||||
<div v-else class="settings-view">
|
|
||||||
<div class="view-row"><span>Host:</span> <b>{{ form.dbHost }}</b></div>
|
|
||||||
<div class="view-row"><span>Port:</span> <b>{{ form.dbPort }}</b></div>
|
|
||||||
<div class="view-row"><span>Database:</span> <b>{{ form.dbName }}</b></div>
|
|
||||||
<div class="view-row"><span>User:</span> <b>{{ form.dbUser }}</b></div>
|
|
||||||
<div class="view-row"><span>Password:</span> <b>••••••••••••••••••••••••••••••••</b></div>
|
|
||||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
|
||||||
<button type="button" class="cancel-btn" @click="$emit('cancel')">Закрыть</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { reactive, ref, onMounted } from 'vue';
|
|
||||||
import api from '@/api/axios';
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
dbHost: '',
|
|
||||||
dbPort: 5432,
|
|
||||||
dbName: '',
|
|
||||||
dbUser: '',
|
|
||||||
dbPassword: ''
|
|
||||||
});
|
|
||||||
const original = reactive({});
|
|
||||||
const editMode = ref(false);
|
|
||||||
|
|
||||||
const loadDbSettings = async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.get('/api/db-settings');
|
|
||||||
if (res.data.success) {
|
|
||||||
const s = res.data.settings;
|
|
||||||
form.dbHost = s.db_host;
|
|
||||||
form.dbPort = s.db_port;
|
|
||||||
form.dbName = s.db_name;
|
|
||||||
form.dbUser = s.db_user;
|
|
||||||
form.dbPassword = '';
|
|
||||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// обработка ошибки
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadDbSettings();
|
|
||||||
editMode.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveDbSettings = async () => {
|
|
||||||
try {
|
|
||||||
await api.put('/api/db-settings', {
|
|
||||||
db_host: form.dbHost,
|
|
||||||
db_port: form.dbPort,
|
|
||||||
db_name: form.dbName,
|
|
||||||
db_user: form.dbUser,
|
|
||||||
db_password: form.dbPassword || undefined
|
|
||||||
});
|
|
||||||
alert('Настройки базы данных сохранены');
|
|
||||||
form.dbPassword = '';
|
|
||||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
|
||||||
editMode.value = false;
|
|
||||||
} catch (e) {
|
|
||||||
alert('Ошибка сохранения настроек базы данных');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelEdit = () => {
|
|
||||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
|
||||||
form.dbPassword = '';
|
|
||||||
editMode.value = false;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.settings-panel {
|
|
||||||
padding: var(--block-padding);
|
|
||||||
background-color: var(--color-light);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
.settings-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.save-btn {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.save-btn:hover {
|
|
||||||
background: var(--color-primary-dark);
|
|
||||||
}
|
|
||||||
.cancel-btn {
|
|
||||||
background: #eee;
|
|
||||||
color: #333;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
.settings-view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.2rem;
|
|
||||||
}
|
|
||||||
.view-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: #f8f8f8;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
.edit-btn {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.edit-btn:hover {
|
|
||||||
background: var(--color-primary-dark);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="email-settings settings-panel">
|
|
||||||
<h2>Настройки Email</h2>
|
|
||||||
<form v-if="editMode" @submit.prevent="saveEmailSettings" class="settings-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="smtpHost">SMTP Host</label>
|
|
||||||
<input id="smtpHost" v-model="form.smtpHost" type="text" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="smtpPort">SMTP Port</label>
|
|
||||||
<input id="smtpPort" v-model.number="form.smtpPort" type="number" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="smtpUser">SMTP User</label>
|
|
||||||
<input id="smtpUser" v-model="form.smtpUser" type="text" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="smtpPassword">SMTP Password</label>
|
|
||||||
<input id="smtpPassword" v-model="form.smtpPassword" type="password" :placeholder="form.smtpPassword ? 'Изменить пароль' : 'Введите пароль'" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="imapHost">IMAP Host</label>
|
|
||||||
<input id="imapHost" v-model="form.imapHost" type="text" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="imapPort">IMAP Port</label>
|
|
||||||
<input id="imapPort" v-model.number="form.imapPort" type="number" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="fromEmail">From Email</label>
|
|
||||||
<input id="fromEmail" v-model="form.fromEmail" type="email" required />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="save-btn">Сохранить</button>
|
|
||||||
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
|
||||||
</form>
|
|
||||||
<div v-else class="settings-view">
|
|
||||||
<div class="view-row"><span>SMTP Host:</span> <b>{{ form.smtpHost }}</b></div>
|
|
||||||
<div class="view-row"><span>SMTP Port:</span> <b>{{ form.smtpPort }}</b></div>
|
|
||||||
<div class="view-row"><span>SMTP User:</span> <b>{{ form.smtpUser }}</b></div>
|
|
||||||
<div class="view-row"><span>IMAP Host:</span> <b>{{ form.imapHost }}</b></div>
|
|
||||||
<div class="view-row"><span>IMAP Port:</span> <b>{{ form.imapPort }}</b></div>
|
|
||||||
<div class="view-row"><span>From Email:</span> <b>{{ form.fromEmail }}</b></div>
|
|
||||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
|
||||||
<button type="button" class="cancel-btn" @click="$emit('cancel')">Закрыть</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { reactive, ref, onMounted } from 'vue';
|
|
||||||
import api from '@/api/axios';
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
smtpHost: '',
|
|
||||||
smtpPort: 465,
|
|
||||||
smtpUser: '',
|
|
||||||
smtpPassword: '',
|
|
||||||
imapHost: '',
|
|
||||||
imapPort: 993,
|
|
||||||
fromEmail: ''
|
|
||||||
});
|
|
||||||
const original = reactive({});
|
|
||||||
const editMode = ref(false);
|
|
||||||
|
|
||||||
const loadEmailSettings = async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.get('/api/email-settings');
|
|
||||||
if (res.data.success) {
|
|
||||||
const s = res.data.settings;
|
|
||||||
form.smtpHost = s.smtp_host;
|
|
||||||
form.smtpPort = s.smtp_port;
|
|
||||||
form.smtpUser = s.smtp_user;
|
|
||||||
form.imapHost = s.imap_host || '';
|
|
||||||
form.imapPort = s.imap_port || 993;
|
|
||||||
form.fromEmail = s.from_email;
|
|
||||||
form.smtpPassword = '';
|
|
||||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// обработка ошибки
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadEmailSettings();
|
|
||||||
editMode.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveEmailSettings = async () => {
|
|
||||||
try {
|
|
||||||
await api.put('/api/email-settings', {
|
|
||||||
smtp_host: form.smtpHost,
|
|
||||||
smtp_port: form.smtpPort,
|
|
||||||
smtp_user: form.smtpUser,
|
|
||||||
smtp_password: form.smtpPassword || undefined,
|
|
||||||
imap_host: form.imapHost,
|
|
||||||
imap_port: form.imapPort,
|
|
||||||
from_email: form.fromEmail
|
|
||||||
});
|
|
||||||
alert('Настройки Email сохранены');
|
|
||||||
form.smtpPassword = '';
|
|
||||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
|
||||||
editMode.value = false;
|
|
||||||
} catch (e) {
|
|
||||||
alert('Ошибка сохранения email-настроек');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelEdit = () => {
|
|
||||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
|
||||||
form.smtpPassword = '';
|
|
||||||
editMode.value = false;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.settings-panel {
|
|
||||||
padding: var(--block-padding);
|
|
||||||
background-color: var(--color-light);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
.settings-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.save-btn {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.save-btn:hover {
|
|
||||||
background: var(--color-primary-dark);
|
|
||||||
}
|
|
||||||
.cancel-btn {
|
|
||||||
background: #eee;
|
|
||||||
color: #333;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
.settings-view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.2rem;
|
|
||||||
}
|
|
||||||
.view-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: #f8f8f8;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
.edit-btn {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.edit-btn:hover {
|
|
||||||
background: var(--color-primary-dark);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="interface-settings settings-panel">
|
<div class="interface-settings settings-panel" style="position:relative;min-height:120px">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
<h2>Настройки Интерфейса</h2>
|
<h2>Настройки Интерфейса</h2>
|
||||||
|
|
||||||
<!-- Панель Язык -->
|
<!-- Панель Язык -->
|
||||||
@@ -27,9 +28,13 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { getFromStorage, setToStorage } from '../../utils/storage'; // Путь к utils может отличаться
|
import { getFromStorage, setToStorage } from '../../utils/storage'; // Путь к utils может отличаться
|
||||||
import DomainConnectBlock from './DomainConnectBlock.vue';
|
import DomainConnectBlock from './DomainConnectBlock.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
// TODO: Импортировать API для сохранения, если нужно
|
// TODO: Импортировать API для сохранения, если нужно
|
||||||
|
|
||||||
const selectedLanguage = ref(getFromStorage('userLanguage', 'ru'));
|
const selectedLanguage = ref(getFromStorage('userLanguage', 'ru'));
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goBack = () => router.push('/settings');
|
||||||
|
|
||||||
// Функция сохранения
|
// Функция сохранения
|
||||||
const saveLanguageSetting = () => {
|
const saveLanguageSetting = () => {
|
||||||
@@ -87,6 +92,21 @@ h3 {
|
|||||||
.btn-primary {
|
.btn-primary {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="ollama-settings settings-panel">
|
|
||||||
<h2>Настройки Ollama</h2>
|
|
||||||
<div class="current-model-block">
|
|
||||||
<span>Текущая модель:</span>
|
|
||||||
<b>{{ currentModel }}</b>
|
|
||||||
</div>
|
|
||||||
<div class="select-model-block">
|
|
||||||
<label for="ollamaModel">Доступные модели для загрузки:</label>
|
|
||||||
<select id="ollamaModel" v-model="selectedModel">
|
|
||||||
<option v-for="model in availableModels" :key="model" :value="model">{{ model }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="close-btn" @click="$emit('cancel')">Закрыть</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
// TODO: заменить на реальный API Ollama
|
|
||||||
const currentModel = ref('qwen2.5');
|
|
||||||
const availableModels = ref(['qwen2.5', 'llama3', 'mistral', 'phi3', 'gemma']);
|
|
||||||
const selectedModel = ref(currentModel.value);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// Здесь будет запрос к Ollama для получения списка моделей и текущей
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.ollama-settings.settings-panel {
|
|
||||||
padding: var(--block-padding);
|
|
||||||
background-color: var(--color-light);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
.current-model-block {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
.select-model-block {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.close-btn {
|
|
||||||
background: #eee;
|
|
||||||
color: #333;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.close-btn:hover {
|
|
||||||
background: #ddd;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!-- Компонент удалён как неактуальный -->
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="security-settings settings-panel">
|
<div class="security-settings settings-panel">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
<h2>Настройки безопасности и подключения к блокчейну</h2>
|
<h2>Настройки безопасности и подключения к блокчейну</h2>
|
||||||
|
|
||||||
<!-- Индикатор загрузки -->
|
<!-- Индикатор загрузки -->
|
||||||
@@ -59,6 +60,7 @@ import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
|
|||||||
import eventBus from '@/utils/eventBus';
|
import eventBus from '@/utils/eventBus';
|
||||||
import RpcProvidersSettings from './RpcProvidersSettings.vue';
|
import RpcProvidersSettings from './RpcProvidersSettings.vue';
|
||||||
import AuthTokensSettings from './AuthTokensSettings.vue';
|
import AuthTokensSettings from './AuthTokensSettings.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
// Состояние для отображения/скрытия дополнительных настроек
|
// Состояние для отображения/скрытия дополнительных настроек
|
||||||
const showRpcSettings = ref(false);
|
const showRpcSettings = ref(false);
|
||||||
@@ -288,6 +290,9 @@ provide('removeAuthToken', removeAuthToken);
|
|||||||
provide('addAuthToken', addAuthToken);
|
provide('addAuthToken', addAuthToken);
|
||||||
provide('newAuthToken', newAuthToken);
|
provide('newAuthToken', newAuthToken);
|
||||||
provide('networks', networks);
|
provide('networks', networks);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const goBack = () => router.push('/settings');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -573,4 +578,20 @@ small {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
65
frontend/src/views/settings/SettingsIndexView.vue
Normal file
65
frontend/src/views/settings/SettingsIndexView.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main-blocks">
|
||||||
|
<div class="main-block">
|
||||||
|
<h3>ИИ</h3>
|
||||||
|
<p>Настройки интеграций, моделей, ассистента и RAG.</p>
|
||||||
|
<button class="details-btn" @click="$router.push('/settings/ai')">Подробнее</button>
|
||||||
|
</div>
|
||||||
|
<div class="main-block">
|
||||||
|
<h3>Блокчейн</h3>
|
||||||
|
<p>Интеграция с блокчейн-сетями, RPC, токены и смарт-контракты.</p>
|
||||||
|
<button class="details-btn" @click="$router.push('/settings/blockchain')">Подробнее</button>
|
||||||
|
</div>
|
||||||
|
<div class="main-block">
|
||||||
|
<h3>Безопасность</h3>
|
||||||
|
<p>Управление доступом, токенами, аутентификацией и правами.</p>
|
||||||
|
<button class="details-btn" @click="$router.push('/settings/security')">Подробнее</button>
|
||||||
|
</div>
|
||||||
|
<div class="main-block">
|
||||||
|
<h3>Интерфейс</h3>
|
||||||
|
<p>Настройки внешнего вида, локализации и пользовательского опыта.</p>
|
||||||
|
<button class="details-btn" @click="$router.push('/settings/interface')">Подробнее</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// пусто
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main-blocks {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.main-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
|
padding: 2rem;
|
||||||
|
min-width: 260px;
|
||||||
|
flex: 1 1 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.details-btn {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.details-btn:hover {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="telegram-settings settings-panel">
|
|
||||||
<h2>Настройки Telegram</h2>
|
|
||||||
<form v-if="editMode" @submit.prevent="saveTelegramSettings" class="settings-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="botToken">Bot Token</label>
|
|
||||||
<input id="botToken" v-model="form.botToken" type="text" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="botUsername">Bot Username</label>
|
|
||||||
<input id="botUsername" v-model="form.botUsername" type="text" required />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="save-btn">Сохранить</button>
|
|
||||||
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
|
||||||
</form>
|
|
||||||
<div v-else class="settings-view">
|
|
||||||
<div class="view-row"><span>Bot Token:</span> <b>••••••••••••••••••••••••••••••••</b></div>
|
|
||||||
<div class="view-row"><span>Bot Username:</span> <b>{{ form.botUsername }}</b></div>
|
|
||||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
|
||||||
<button type="button" class="cancel-btn" @click="$emit('cancel')">Закрыть</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { reactive, ref, onMounted } from 'vue';
|
|
||||||
import api from '@/api/axios';
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
botToken: '',
|
|
||||||
botUsername: ''
|
|
||||||
});
|
|
||||||
const original = reactive({});
|
|
||||||
const editMode = ref(false);
|
|
||||||
|
|
||||||
const loadTelegramSettings = async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.get('/api/telegram-settings');
|
|
||||||
if (res.data.success) {
|
|
||||||
const s = res.data.settings;
|
|
||||||
form.botToken = '';
|
|
||||||
form.botUsername = s.bot_username;
|
|
||||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// обработка ошибки
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadTelegramSettings();
|
|
||||||
editMode.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveTelegramSettings = async () => {
|
|
||||||
try {
|
|
||||||
await api.put('/api/telegram-settings', {
|
|
||||||
bot_token: form.botToken,
|
|
||||||
bot_username: form.botUsername
|
|
||||||
});
|
|
||||||
alert('Настройки Telegram сохранены');
|
|
||||||
form.botToken = '';
|
|
||||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
|
||||||
editMode.value = false;
|
|
||||||
} catch (e) {
|
|
||||||
alert('Ошибка сохранения telegram-настроек');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelEdit = () => {
|
|
||||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
|
||||||
form.botToken = '';
|
|
||||||
editMode.value = false;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.settings-panel {
|
|
||||||
padding: var(--block-padding);
|
|
||||||
background-color: var(--color-light);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
.settings-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.save-btn {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.save-btn:hover {
|
|
||||||
background: var(--color-primary-dark);
|
|
||||||
}
|
|
||||||
.cancel-btn {
|
|
||||||
background: #eee;
|
|
||||||
color: #333;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
.settings-view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.2rem;
|
|
||||||
}
|
|
||||||
.view-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: #f8f8f8;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
.edit-btn {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.edit-btn:hover {
|
|
||||||
background: var(--color-primary-dark);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,13 +7,14 @@
|
|||||||
- Поддерживает фильтрацию по продуктам, сегментам клиентов (тегам), приоритету, дате и другим бизнес-полям.
|
- Поддерживает фильтрацию по продуктам, сегментам клиентов (тегам), приоритету, дате и другим бизнес-полям.
|
||||||
- Интегрируется с LLM (Ollama/OpenAI) для генерации финального ответа на основе найденного контекста.
|
- Интегрируется с LLM (Ollama/OpenAI) для генерации финального ответа на основе найденного контекста.
|
||||||
- Позволяет настраивать системный промт с плейсхолдерами для гибкой персонализации ответов.
|
- Позволяет настраивать системный промт с плейсхолдерами для гибкой персонализации ответов.
|
||||||
|
- Позволяет генерировать draft-ответ для администратора, который может быть отредактирован и отправлен вручную.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Основные требования
|
## Основные требования
|
||||||
|
|
||||||
1. **Структура RAG-таблицы**
|
1. **Гибкая структура RAG-таблицы**
|
||||||
- Каждая строка содержит:
|
- Пользователь может создавать таблицы с произвольными столбцами и назначать им специальное значение (purpose) через выпадающий список:
|
||||||
- Вопрос (`question`)
|
- Вопрос (`question`)
|
||||||
- Ответ (`answer`)
|
- Ответ (`answer`)
|
||||||
- Ответ с уточняющим вопросом (`clarifyingAnswer`)
|
- Ответ с уточняющим вопросом (`clarifyingAnswer`)
|
||||||
@@ -23,12 +24,13 @@
|
|||||||
- Дополнительный контекст (`context`)
|
- Дополнительный контекст (`context`)
|
||||||
- Приоритет (`priority`)
|
- Приоритет (`priority`)
|
||||||
- Дата (`date`)
|
- Дата (`date`)
|
||||||
- Для каждого столбца указывается назначение (purpose) через выпадающий список при создании/редактировании.
|
- Для каждого столбца указывается назначение при создании/редактировании.
|
||||||
|
- Можно добавлять дополнительные столбцы для бизнес-атрибутов.
|
||||||
|
|
||||||
2. **Фильтрация и поиск**
|
2. **Фильтрация и поиск**
|
||||||
- При поступлении вопроса пользователя:
|
- При поступлении вопроса пользователя:
|
||||||
- Фильтровать строки по продукту, тегам пользователя, приоритету, дате и другим полям.
|
- Фильтровать строки по продукту, тегам пользователя, приоритету, дате и другим полям.
|
||||||
- Выполнять векторный поиск (embedding) только по релевантным строкам.
|
- Выполнять векторный поиск (embedding) только по релевантным строкам (по столбцу "Вопрос").
|
||||||
|
|
||||||
3. **Интеграция с LLM**
|
3. **Интеграция с LLM**
|
||||||
- После поиска по RAG-таблице формировать системный промт с подстановкой найденных данных (через плейсхолдеры).
|
- После поиска по RAG-таблице формировать системный промт с подстановкой найденных данных (через плейсхолдеры).
|
||||||
@@ -59,6 +61,11 @@
|
|||||||
- Логируются все этапы работы ассистента: запрос пользователя, найденный контекст, результат LLM, время ответа, id пользователя и т.д.
|
- Логируются все этапы работы ассистента: запрос пользователя, найденный контекст, результат LLM, время ответа, id пользователя и т.д.
|
||||||
- Вся информация сохраняется для последующего анализа и улучшения качества ответов.
|
- Вся информация сохраняется для последующего анализа и улучшения качества ответов.
|
||||||
|
|
||||||
|
7. **Автоответ и draft-ответ**
|
||||||
|
- Если найден релевантный вопрос (score > 0.95) — ассистент автоматически отвечает пользователю.
|
||||||
|
- Если нет — ассистент генерирует draft-ответ для администратора (через роут `/ai-draft`), который подставляется в поле ввода и ожидает отправки админом. Draft генерируется всегда, даже если нет точного совпадения в RAG.
|
||||||
|
- Draft-ответ строится с учётом промта, истории и всех доступных данных из RAG.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Пример бизнес-сценария
|
## Пример бизнес-сценария
|
||||||
@@ -69,6 +76,7 @@
|
|||||||
- Векторный поиск проводится только по релевантным строкам.
|
- Векторный поиск проводится только по релевантным строкам.
|
||||||
- В системном промте используются плейсхолдеры для подстановки найденных данных.
|
- В системном промте используются плейсхолдеры для подстановки найденных данных.
|
||||||
- LLM генерирует финальный ответ с учётом контекста, уточняющих вопросов и ответов на возражения.
|
- LLM генерирует финальный ответ с учётом контекста, уточняющих вопросов и ответов на возражения.
|
||||||
|
- Если точного совпадения нет — draft-ответ для администратора формируется на основе промта и истории.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -90,3 +98,4 @@
|
|||||||
- Персонализированные, точные и масштабируемые ответы для разных продуктов и сегментов клиентов.
|
- Персонализированные, точные и масштабируемые ответы для разных продуктов и сегментов клиентов.
|
||||||
- Гибкая настройка ассистента через UI и системный промт.
|
- Гибкая настройка ассистента через UI и системный промт.
|
||||||
- Возможность расширения под любые бизнес-сценарии.
|
- Возможность расширения под любые бизнес-сценарии.
|
||||||
|
- Draft-ответы для администратора, которые можно редактировать и отправлять вручную.
|
||||||
51
md/RAG_SEARCH_SETUP_AND_TEST.md
Normal file
51
md/RAG_SEARCH_SETUP_AND_TEST.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# План настройки и тестирования поиска по таблице RAG
|
||||||
|
|
||||||
|
## 1. Подготовка таблицы RAG
|
||||||
|
- Убедиться, что таблица RAG создана и содержит пары "вопрос-ответ".
|
||||||
|
- Добавить несколько тестовых записей через UI или напрямую в базу данных.
|
||||||
|
|
||||||
|
## 2. Настройка провайдера эмбеддингов
|
||||||
|
- В настройках ассистента выбрать нужного провайдера (OpenAI, Ollama и др.).
|
||||||
|
- Ввести API-ключ и Base URL (например, для OpenAI: https://api.openai.com/v1).
|
||||||
|
- Сохранить настройки.
|
||||||
|
|
||||||
|
## 3. Проверка настроек ассистента
|
||||||
|
- Убедиться, что выбран актуальный ID таблицы RAG.
|
||||||
|
- Проверить выбранного провайдера эмбеддингов.
|
||||||
|
- Установить порог релевантности (например, 0.95).
|
||||||
|
|
||||||
|
## 4. Проверка backend-логики
|
||||||
|
- Проверить, что в backend (например, в ragService.js) реализован поиск по RAG с использованием выбранного провайдера эмбеддингов.
|
||||||
|
- Убедиться, что используется актуальный ID таблицы и динамический выбор провайдера.
|
||||||
|
- Проверить возможность изменения порога релевантности.
|
||||||
|
|
||||||
|
## 5. Тестирование через UI
|
||||||
|
- Отправить ассистенту вопрос, который есть в RAG-таблице — убедиться, что ответ возвращается из базы.
|
||||||
|
- Отправить вопрос, которого нет в таблице — убедиться, что ассистент либо не отвечает, либо использует LLM (по настройкам).
|
||||||
|
|
||||||
|
## 6. Проверка логов backend
|
||||||
|
- Проверить логи на наличие сообщений о поиске по RAG, найденных совпадениях и выбранном провайдере эмбеддингов.
|
||||||
|
- В случае ошибок — проанализировать и устранить их.
|
||||||
|
|
||||||
|
## 7. Тестирование через API (опционально)
|
||||||
|
- Использовать Postman/curl для отправки запросов напрямую к backend.
|
||||||
|
- Пример запроса:
|
||||||
|
```http
|
||||||
|
POST /api/chat/message
|
||||||
|
{
|
||||||
|
"userId": 137,
|
||||||
|
"message": "Как зовут?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Автоматизация тестирования (по желанию)
|
||||||
|
- Написать автотесты (например, на Mocha/Jest), которые будут отправлять вопросы и сверять ответы с ожидаемыми из RAG.
|
||||||
|
|
||||||
|
## 9. Рекомендации
|
||||||
|
- Для тестов использовать уникальные, простые вопросы и ответы.
|
||||||
|
- После каждого изменения настроек проводить тестовые запросы.
|
||||||
|
- Добавить в UI индикатор источника ответа (из базы или сгенерирован).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Если потребуется пример кода или помощь с конкретной реализацией — обращайтесь!**
|
||||||
Reference in New Issue
Block a user