ваше сообщение коммита
This commit is contained in:
@@ -15,6 +15,12 @@
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="message.content" class="message-content" v-html="formattedContent" />
|
||||
|
||||
<!-- Кнопки для системного сообщения -->
|
||||
<div v-if="message.sender_type === 'system' && (message.telegramBotUrl || message.supportEmail)" class="system-actions">
|
||||
<button v-if="message.telegramBotUrl" @click="openTelegram(message.telegramBotUrl)" class="system-btn">Перейти в Telegram-бот</button>
|
||||
<button v-if="message.supportEmail" @click="copyEmail(message.supportEmail)" class="system-btn">Скопировать email</button>
|
||||
</div>
|
||||
|
||||
<!-- Блок для отображения прикрепленного файла (теперь с плеерами/изображением/ссылкой) -->
|
||||
<div v-if="attachment" class="message-attachments">
|
||||
<div class="attachment-item">
|
||||
@@ -168,6 +174,14 @@ const formatFileSize = (bytes) => {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
function openTelegram(url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
function copyEmail(email) {
|
||||
navigator.clipboard.writeText(email);
|
||||
// Можно добавить уведомление "Email скопирован"
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -360,4 +374,23 @@ const formatFileSize = (bytes) => {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.system-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.system-btn {
|
||||
background: var(--color-primary, #3b82f6);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.system-btn:hover {
|
||||
background: var(--color-primary-dark, #2563eb);
|
||||
}
|
||||
</style>
|
||||
122
frontend/src/components/ai-assistant/RuleEditor.vue
Normal file
122
frontend/src/components/ai-assistant/RuleEditor.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class='modal-bg'>
|
||||
<div class='modal'>
|
||||
<h3>{{ rule ? 'Редактировать' : 'Создать' }} набор правил</h3>
|
||||
<label>Название</label>
|
||||
<input v-model="name" />
|
||||
<label>Описание</label>
|
||||
<textarea v-model="description" rows="3" placeholder="Опишите правило в свободной форме" />
|
||||
<button type="button" @click="convertToJson" style="margin: 0.5rem 0;">Преобразовать в JSON</button>
|
||||
<label>Правила (JSON)</label>
|
||||
<textarea v-model="rulesJson" rows="6"></textarea>
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div class="actions">
|
||||
<button @click="save">Сохранить</button>
|
||||
<button @click="close">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
const emit = defineEmits(['close']);
|
||||
const props = defineProps({ rule: Object });
|
||||
const name = ref(props.rule ? props.rule.name : '');
|
||||
const description = ref(props.rule ? props.rule.description : '');
|
||||
const rulesJson = ref(props.rule ? JSON.stringify(props.rule.rules, null, 2) : '{\n "checkUserTags": true\n}');
|
||||
const error = ref('');
|
||||
|
||||
watch(() => props.rule, (newRule) => {
|
||||
name.value = newRule ? newRule.name : '';
|
||||
description.value = newRule ? newRule.description : '';
|
||||
rulesJson.value = newRule ? JSON.stringify(newRule.rules, null, 2) : '{\n "checkUserTags": true\n}';
|
||||
});
|
||||
|
||||
function convertToJson() {
|
||||
// Простейший пример: если в описании есть "теги", выставляем checkUserTags
|
||||
// В реальном проекте здесь можно интегрировать LLM или шаблоны
|
||||
try {
|
||||
if (/тег[а-я]* пользов/.test(description.value.toLowerCase())) {
|
||||
rulesJson.value = JSON.stringify({ checkUserTags: true }, null, 2);
|
||||
error.value = '';
|
||||
} else {
|
||||
rulesJson.value = JSON.stringify({ customRule: description.value }, null, 2);
|
||||
error.value = '';
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = 'Не удалось преобразовать описание в JSON';
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
let rules;
|
||||
try {
|
||||
rules = JSON.parse(rulesJson.value);
|
||||
} catch (e) {
|
||||
error.value = 'Ошибка в формате JSON!';
|
||||
return;
|
||||
}
|
||||
if (props.rule && props.rule.id) {
|
||||
await axios.put(`/api/settings/ai-assistant-rules/${props.rule.id}`, { name: name.value, description: description.value, rules });
|
||||
} else {
|
||||
await axios.post('/api/settings/ai-assistant-rules', { name: name.value, description: description.value, rules });
|
||||
}
|
||||
emit('close', true);
|
||||
}
|
||||
function close() { emit('close', false); }
|
||||
</script>
|
||||
<style scoped>
|
||||
.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;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
button {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
button:last-child {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
}
|
||||
.error {
|
||||
color: #c00;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,15 @@ import api from '../api/axios';
|
||||
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
||||
import { generateUniqueId } from '../utils/helpers';
|
||||
|
||||
function initGuestId() {
|
||||
let id = getFromStorage('guestId', '');
|
||||
if (!id) {
|
||||
id = generateUniqueId();
|
||||
setToStorage('guestId', id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function useChat(auth) {
|
||||
const messages = ref([]);
|
||||
const newMessage = ref('');
|
||||
@@ -20,7 +29,7 @@ export function useChat(auth) {
|
||||
isLinkingGuest: false, // Флаг для процесса связывания гостевых сообщений (пока не используется активно)
|
||||
});
|
||||
|
||||
const guestId = ref(getFromStorage('guestId', ''));
|
||||
const guestId = ref(initGuestId());
|
||||
|
||||
const shouldLoadHistory = computed(() => {
|
||||
return auth.isAuthenticated.value || !!guestId.value;
|
||||
@@ -133,7 +142,7 @@ export function useChat(auth) {
|
||||
// Очищаем гостевые данные после успешной аутентификации и загрузки
|
||||
if (authType) {
|
||||
removeFromStorage('guestMessages');
|
||||
removeFromStorage('guestId');
|
||||
// removeFromStorage('guestId'); // Удаление guestId теперь только после успешного связывания
|
||||
guestId.value = '';
|
||||
}
|
||||
|
||||
@@ -219,7 +228,7 @@ export function useChat(auth) {
|
||||
let apiUrl = '/api/chat/message';
|
||||
if (isGuestMessage) {
|
||||
if (!guestId.value) {
|
||||
guestId.value = generateUniqueId();
|
||||
guestId.value = initGuestId();
|
||||
setToStorage('guestId', guestId.value);
|
||||
}
|
||||
formData.append('guestId', guestId.value);
|
||||
@@ -254,6 +263,20 @@ export function useChat(auth) {
|
||||
});
|
||||
}
|
||||
|
||||
// Добавляем системное сообщение для гостя (только на клиенте, не сохраняется в истории)
|
||||
if (isGuestMessage && response.data.systemMessage) {
|
||||
messages.value.push({
|
||||
id: `system-${Date.now()}`,
|
||||
content: response.data.systemMessage,
|
||||
sender_type: 'system',
|
||||
role: 'system',
|
||||
timestamp: new Date().toISOString(),
|
||||
isSystem: true,
|
||||
telegramBotUrl: response.data.telegramBotUrl,
|
||||
supportEmail: response.data.supportEmail
|
||||
});
|
||||
}
|
||||
|
||||
// Сохраняем гостевое сообщение (если нужно)
|
||||
// В текущей реализации HomeView гостевые сообщения из localstorage загружаются только при старте
|
||||
// Если нужна синхронизация после отправки, логику нужно добавить/изменить
|
||||
@@ -325,6 +348,23 @@ export function useChat(auth) {
|
||||
}
|
||||
};
|
||||
|
||||
// --- Связывание гостевых сообщений после аутентификации ---
|
||||
const linkGuestMessagesAfterAuth = async () => {
|
||||
if (!guestId.value) return;
|
||||
try {
|
||||
const response = await api.post('/api/chat/process-guest', { guestId: guestId.value });
|
||||
if (response.data.success && response.data.conversationId) {
|
||||
// Можно сразу загрузить историю по этому диалогу, если нужно
|
||||
await loadMessages({ initial: true });
|
||||
// Удаляем guestId только после успешного связывания
|
||||
removeFromStorage('guestId');
|
||||
guestId.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[useChat] Ошибка связывания гостевых сообщений:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Watchers ---
|
||||
// Сортировка сообщений при изменении
|
||||
watch(messages, (newMessages) => {
|
||||
@@ -379,5 +419,6 @@ export function useChat(auth) {
|
||||
loadMessages,
|
||||
handleSendMessage,
|
||||
loadGuestMessagesFromStorage, // Экспортируем на всякий случай
|
||||
linkGuestMessagesAfterAuth, // Экспортируем для вызова после авторизации
|
||||
};
|
||||
}
|
||||
@@ -64,6 +64,7 @@
|
||||
messageLoading,
|
||||
loadMessages,
|
||||
handleSendMessage,
|
||||
linkGuestMessagesAfterAuth,
|
||||
} = useChat(auth);
|
||||
|
||||
// =====================================================================
|
||||
@@ -91,10 +92,12 @@
|
||||
// =====================================================================
|
||||
|
||||
// Функция обновления сообщений после авторизации
|
||||
const handleAuthEvent = (eventData) => {
|
||||
const handleAuthEvent = async (eventData) => {
|
||||
console.log('[HomeView] Получено событие изменения авторизации:', eventData);
|
||||
if (eventData.isAuthenticated) {
|
||||
// Пользователь только что авторизовался - загрузим сообщения
|
||||
// Сначала связываем гостевые сообщения, если есть
|
||||
await linkGuestMessagesAfterAuth();
|
||||
// Затем загружаем сообщения (если не было гостя, просто загрузка)
|
||||
loadMessages({ initial: true, authType: eventData.authType || 'wallet' });
|
||||
} else {
|
||||
// Пользователь вышел из системы - можно очистить или обновить данные
|
||||
|
||||
211
frontend/src/views/settings/AiAssistantSettings.vue
Normal file
211
frontend/src/views/settings/AiAssistantSettings.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<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>
|
||||
<select v-model="settings.selected_rag_tables" multiple>
|
||||
<option v-for="table in userTables" :key="table.id" :value="table.id">
|
||||
{{ table.name }}
|
||||
</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="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 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;
|
||||
}
|
||||
</style>
|
||||
@@ -37,6 +37,11 @@
|
||||
<p>Интеграция с PostgreSQL для хранения данных приложения и управления настройками.</p>
|
||||
<button class="details-btn" @click="showDbSettings = true">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>ИИ-ассистент</h3>
|
||||
<p>Настройки поведения, языков, моделей и правил работы ассистента.</p>
|
||||
<button class="details-btn" @click="showAiAssistantSettings = true">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
<AIProviderSettings
|
||||
v-if="showProvider"
|
||||
@@ -52,6 +57,7 @@
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -61,10 +67,13 @@ 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 showTelegramSettings = ref(false);
|
||||
const showEmailSettings = ref(false);
|
||||
const showDbSettings = ref(false);
|
||||
const showAiAssistantSettings = ref(false);
|
||||
|
||||
const providerLabels = {
|
||||
openai: {
|
||||
|
||||
Reference in New Issue
Block a user