ваше сообщение коммита
This commit is contained in:
@@ -20,6 +20,12 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
# Передача cookies и сессии
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass_request_body on;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
}
|
||||
|
||||
# Проксирование к development серверу frontend
|
||||
|
||||
@@ -46,28 +46,68 @@
|
||||
|
||||
<div class="rag-test-section">
|
||||
<h4>🧠 Тест RAG-функциональности</h4>
|
||||
|
||||
|
||||
|
||||
<!-- Выбор RAG-таблицы -->
|
||||
<div class="rag-table-selection">
|
||||
<label>Выберите RAG-таблицу:</label>
|
||||
<div class="rag-table-controls">
|
||||
<select v-model="selectedRagTable" class="rag-table-select">
|
||||
<option v-if="availableRagTables.length === 0" value="" disabled>
|
||||
Нет доступных RAG-таблиц
|
||||
</option>
|
||||
<option v-for="table in availableRagTables" :key="table.id" :value="table.id">
|
||||
{{ table.name }} (ID: {{ table.id }})
|
||||
</option>
|
||||
</select>
|
||||
<button @click="loadRagTables" class="refresh-tables-btn" title="Обновить список таблиц">
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="availableRagTables.length === 0" class="no-rag-tables">
|
||||
<p>Для тестирования RAG необходимо создать таблицу и установить её как источник для ИИ-ассистента.</p>
|
||||
<p>Перейдите в <router-link to="/tables">Таблицы</router-link> и создайте таблицу с вопросами и ответами.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rag-test-controls">
|
||||
<input
|
||||
v-model="ragQuestion"
|
||||
placeholder="Введите вопрос"
|
||||
class="rag-input"
|
||||
/>
|
||||
<button @click="testRAG" :disabled="ragTesting" class="rag-test-btn">
|
||||
<button @click="testRAG" :disabled="ragTesting || !selectedRagTable" class="rag-test-btn">
|
||||
{{ ragTesting ? 'Тестирование...' : 'Тестировать RAG' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Прогресс-бар и статус -->
|
||||
<div v-if="ragTesting" class="rag-progress-section">
|
||||
<div class="rag-status">{{ ragStatus }}</div>
|
||||
<div class="rag-progress-bar">
|
||||
<div class="rag-progress-fill" :style="{ width: ragProgress + '%' }"></div>
|
||||
</div>
|
||||
<div class="rag-progress-text">{{ Math.round(ragProgress) }}%</div>
|
||||
</div>
|
||||
|
||||
<div v-if="ragResult" :class="['rag-result', getRagResultClass()]">
|
||||
<div v-if="ragResult.success">
|
||||
<strong>✅ Успешно!</strong><br>
|
||||
Таблица: {{ availableRagTables.find(t => t.id === selectedRagTable)?.name || 'Неизвестно' }}<br>
|
||||
Вопрос: "{{ ragQuestion }}"<br>
|
||||
Ответ: "{{ ragResult.answer || 'Нет ответа' }}"<br>
|
||||
Score: {{ ragResult.score || 'N/A' }}
|
||||
|
||||
</div>
|
||||
<div v-else>
|
||||
<strong>❌ Ошибка!</strong><br>
|
||||
{{ ragResult.error }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -78,10 +118,14 @@ import axios from 'axios';
|
||||
|
||||
const loading = ref(false);
|
||||
const lastUpdate = ref('никогда');
|
||||
const ragQuestion = ref('вопрос 1');
|
||||
const ragQuestion = ref('Как работает ИИ-ассистент?');
|
||||
const ragTesting = ref(false);
|
||||
const ragResult = ref(null);
|
||||
const monitoringData = ref(null);
|
||||
const availableRagTables = ref([]);
|
||||
const selectedRagTable = ref(null);
|
||||
const ragProgress = ref(0);
|
||||
const ragStatus = ref('');
|
||||
|
||||
const serviceLabels = {
|
||||
backend: 'Backend',
|
||||
@@ -127,6 +171,25 @@ const getRagResultClass = () => {
|
||||
return ragResult.value.success ? 'success' : 'error';
|
||||
};
|
||||
|
||||
const loadRagTables = async () => {
|
||||
try {
|
||||
const response = await axios.get('/tables');
|
||||
const tables = response.data || [];
|
||||
|
||||
// Фильтруем только таблицы, которые являются источниками для RAG
|
||||
const ragTables = tables.filter(table => table.is_rag_source_id === 1);
|
||||
|
||||
availableRagTables.value = ragTables;
|
||||
|
||||
// Если есть доступные таблицы, выбираем первую по умолчанию
|
||||
if (availableRagTables.value.length > 0 && !selectedRagTable.value) {
|
||||
selectedRagTable.value = availableRagTables.value[0].id;
|
||||
}
|
||||
} catch (e) {
|
||||
availableRagTables.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const refreshStatus = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -142,31 +205,74 @@ const refreshStatus = async () => {
|
||||
|
||||
const testRAG = async () => {
|
||||
if (!ragQuestion.value.trim()) return;
|
||||
|
||||
if (!selectedRagTable.value) {
|
||||
ragResult.value = {
|
||||
success: false,
|
||||
error: 'Не выбрана RAG-таблица для тестирования'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
ragTesting.value = true;
|
||||
ragResult.value = null;
|
||||
ragProgress.value = 0;
|
||||
ragStatus.value = '🔍 Ищем ответ в базе знаний...';
|
||||
|
||||
// Симуляция прогресса для лучшего UX
|
||||
const progressInterval = setInterval(() => {
|
||||
if (ragProgress.value < 90) {
|
||||
ragProgress.value += Math.random() * 15;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
ragStatus.value = '🤖 Генерируем ответ с помощью ИИ...';
|
||||
|
||||
const response = await axios.post('/rag/answer', {
|
||||
tableId: 28,
|
||||
tableId: selectedRagTable.value,
|
||||
question: ragQuestion.value,
|
||||
product: null
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
ragProgress.value = 100;
|
||||
ragStatus.value = '✅ Готово!';
|
||||
|
||||
ragResult.value = {
|
||||
success: true,
|
||||
answer: response.data.answer,
|
||||
score: response.data.score,
|
||||
llmResponse: response.data.llmResponse
|
||||
};
|
||||
|
||||
// Обновляем список таблиц после успешного тестирования
|
||||
await loadRagTables();
|
||||
} catch (error) {
|
||||
clearInterval(progressInterval);
|
||||
ragProgress.value = 0;
|
||||
ragStatus.value = '';
|
||||
|
||||
ragResult.value = {
|
||||
success: false,
|
||||
error: error.response?.data?.error || error.message || 'Неизвестная ошибка'
|
||||
};
|
||||
}
|
||||
|
||||
ragTesting.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
refreshStatus();
|
||||
loadRagTables();
|
||||
|
||||
// Подписываемся на обновление плейсхолдеров (когда создаются новые таблицы)
|
||||
window.addEventListener('placeholders-updated', loadRagTables);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// Отписываемся от события
|
||||
window.removeEventListener('placeholders-updated', loadRagTables);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -311,6 +417,132 @@ onMounted(() => {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.rag-table-selection {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.rag-table-selection label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.rag-table-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rag-table-select {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.refresh-tables-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.refresh-tables-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.no-rag-tables {
|
||||
padding: 15px;
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 6px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.no-rag-tables p {
|
||||
margin: 5px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-rag-tables a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.no-rag-tables a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.rag-progress-section {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.rag-status {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rag-progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #e9ecef;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rag-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #28a745, #20c997);
|
||||
border-radius: 10px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rag-progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.rag-progress-text {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #28a745;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.rag-test-section h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
|
||||
@@ -168,6 +168,9 @@ function autoResize() {
|
||||
|
||||
watch(editing, (val) => {
|
||||
if (val) {
|
||||
if (props.column.type === 'multiselect-relation') {
|
||||
loadMultiRelationOptions();
|
||||
}
|
||||
nextTick(() => {
|
||||
if (textareaRef.value) {
|
||||
autoResize();
|
||||
@@ -220,6 +223,7 @@ let unsubscribeFromTags = null;
|
||||
// Флаг для предотвращения повторных вызовов
|
||||
let isInitialized = false;
|
||||
let isMultiRelationValuesLoaded = false;
|
||||
let lastLoadedOptionsKey = null;
|
||||
|
||||
onMounted(async () => {
|
||||
const startTime = Date.now();
|
||||
@@ -250,14 +254,12 @@ onMounted(async () => {
|
||||
} else if (props.column.type === 'multiselect-relation') {
|
||||
// Загружаем опции только один раз
|
||||
if (!isInitialized) {
|
||||
// console.log(`[TableCell] 📥 Загружаем опции для row:${props.rowId} col:${props.column.id}`);
|
||||
await loadMultiRelationOptions();
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
// Загружаем relations только один раз для каждой комбинации rowId + columnId
|
||||
if (!isMultiRelationValuesLoaded) {
|
||||
// console.log(`[TableCell] 📥 Загружаем relations для row:${props.rowId} col:${props.column.id}`);
|
||||
await loadMultiRelationValues();
|
||||
isMultiRelationValuesLoaded = true;
|
||||
}
|
||||
@@ -326,6 +328,12 @@ onUnmounted(() => {
|
||||
watch(
|
||||
() => [props.rowId, props.column.id, props.cellValues],
|
||||
async () => {
|
||||
// Сбрасываем флаги при изменении столбца
|
||||
if (props.column.type === 'multiselect-relation') {
|
||||
isMultiRelationValuesLoaded = false;
|
||||
lastLoadedOptionsKey = null;
|
||||
isInitialized = false;
|
||||
}
|
||||
if (props.column.type === 'multiselect') {
|
||||
multiOptions.value = (props.column.options && props.column.options.options) || [];
|
||||
const cell = props.cellValues.find(
|
||||
@@ -485,9 +493,9 @@ async function loadLookupValues() {
|
||||
}
|
||||
|
||||
async function loadMultiRelationOptions() {
|
||||
// Проверяем, не загружены ли уже опции
|
||||
if (multiRelationOptions.value.length > 0) {
|
||||
// console.log('[loadMultiRelationOptions] Опции уже загружены, пропускаем');
|
||||
// Проверяем, не загружены ли уже опции для текущего столбца
|
||||
const cacheKey = `${props.column.id}_${props.column.options?.relatedTableId}`;
|
||||
if (multiRelationOptions.value.length > 0 && lastLoadedOptionsKey === cacheKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -517,7 +525,16 @@ async function loadMultiRelationOptions() {
|
||||
opts.push({ id: row.id, display: cell ? cell.value : `ID ${row.id}` });
|
||||
}
|
||||
multiRelationOptions.value = opts;
|
||||
// console.log(`[loadMultiRelationOptions] Загружено ${opts.length} опций для таблицы ${rel.relatedTableId}`);
|
||||
lastLoadedOptionsKey = cacheKey;
|
||||
|
||||
// Обновляем selectedMultiRelationNames на основе текущих значений
|
||||
if (editMultiRelationValues.value.length > 0) {
|
||||
selectedMultiRelationNames.value = opts
|
||||
.filter(opt => editMultiRelationValues.value.includes(String(opt.id)))
|
||||
.map(opt => opt.display);
|
||||
} else {
|
||||
selectedMultiRelationNames.value = [];
|
||||
}
|
||||
} catch (e) {
|
||||
// console.error('[loadMultiRelationOptions] Error:', e);
|
||||
}
|
||||
@@ -531,7 +548,6 @@ const LOAD_DEBOUNCE_DELAY = 50; // 50ms (уменьшено для ускоре
|
||||
async function loadMultiRelationValues() {
|
||||
// Проверяем, не загружены ли уже данные
|
||||
if (isMultiRelationValuesLoaded) {
|
||||
// console.log('[loadMultiRelationValues] Данные уже загружены, пропускаем');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -603,7 +619,6 @@ async function loadMultiRelationValues() {
|
||||
selectedMultiRelationNames.value = multiRelationOptions.value
|
||||
.filter(opt => relatedRowIds.includes(String(opt.id)))
|
||||
.map(opt => opt.display);
|
||||
// console.log('[loadMultiRelationValues] selectedMultiRelationNames:', selectedMultiRelationNames.value);
|
||||
|
||||
// Отмечаем, что данные загружены
|
||||
isMultiRelationValuesLoaded = true;
|
||||
|
||||
@@ -68,13 +68,17 @@
|
||||
<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>
|
||||
<option value="">Выберите таблицу</option>
|
||||
<option v-for="table in ragTables" :key="table.id" :value="table.id">
|
||||
{{ getTableDisplayName(table) }}
|
||||
</option>
|
||||
</select>
|
||||
<label>Набор правил</label>
|
||||
<div class="rules-row">
|
||||
<select v-model="settings.rules_id">
|
||||
<option value="">Выберите набор правил</option>
|
||||
<option v-for="rule in rulesList" :key="rule.id" :value="rule.id">
|
||||
{{ rule.name }}
|
||||
{{ getRuleDisplayName(rule) }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" @click="openRuleEditor()">Создать</button>
|
||||
@@ -150,7 +154,18 @@ async function loadRules() {
|
||||
async function loadSettings() {
|
||||
const { data } = await axios.get('/settings/ai-assistant');
|
||||
if (data.success && data.settings) {
|
||||
settings.value = data.settings;
|
||||
// Обрабатываем selected_rag_tables - если это массив, берем первый элемент для single select
|
||||
const settingsData = { ...data.settings };
|
||||
if (Array.isArray(settingsData.selected_rag_tables) && settingsData.selected_rag_tables.length > 0) {
|
||||
// Для single select берем первый элемент массива
|
||||
settingsData.selected_rag_tables = settingsData.selected_rag_tables[0];
|
||||
} else if (!Array.isArray(settingsData.selected_rag_tables)) {
|
||||
// Если это не массив, устанавливаем пустое значение
|
||||
settingsData.selected_rag_tables = '';
|
||||
}
|
||||
|
||||
settings.value = settingsData;
|
||||
console.log('[AiAssistantSettings] Loaded settings:', settings.value);
|
||||
}
|
||||
}
|
||||
async function loadTelegramBots() {
|
||||
@@ -204,7 +219,14 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener('placeholders-updated', loadPlaceholders);
|
||||
});
|
||||
async function saveSettings() {
|
||||
await axios.put('/settings/ai-assistant', settings.value);
|
||||
// Преобразуем selected_rag_tables в массив перед сохранением
|
||||
const settingsToSave = { ...settings.value };
|
||||
if (settingsToSave.selected_rag_tables && !Array.isArray(settingsToSave.selected_rag_tables)) {
|
||||
settingsToSave.selected_rag_tables = [settingsToSave.selected_rag_tables];
|
||||
}
|
||||
|
||||
console.log('[AiAssistantSettings] Saving settings:', settingsToSave);
|
||||
await axios.put('/settings/ai-assistant', settingsToSave);
|
||||
goBack();
|
||||
}
|
||||
function openRuleEditor(ruleId = null) {
|
||||
@@ -226,6 +248,16 @@ async function onRuleEditorClose(updated) {
|
||||
editingRule.value = null;
|
||||
if (updated) await loadRules();
|
||||
}
|
||||
|
||||
function getTableDisplayName(table) {
|
||||
if (!table) return '';
|
||||
return table.name || `Таблица ${table.id}`;
|
||||
}
|
||||
|
||||
function getRuleDisplayName(rule) {
|
||||
if (!rule) return '';
|
||||
return rule.name || `Набор правил ${rule.id}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -41,6 +41,14 @@
|
||||
<label for="imapPort">IMAP Port</label>
|
||||
<input id="imapPort" v-model.number="form.imapPort" type="number" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imapUser">IMAP User</label>
|
||||
<input id="imapUser" v-model="form.imapUser" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imapPassword">IMAP Password</label>
|
||||
<input id="imapPassword" v-model="form.imapPassword" type="password" :placeholder="form.imapPassword ? 'Изменить пароль' : 'Введите пароль'" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fromEmail">From Email</label>
|
||||
<input id="fromEmail" v-model="form.fromEmail" type="email" required />
|
||||
@@ -54,6 +62,8 @@
|
||||
<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>IMAP User:</span> <b>{{ form.imapUser }}</b></div>
|
||||
<div class="view-row"><span>IMAP Password:</span> <b>{{ form.imapPassword ? '••••••••' : 'Не установлен' }}</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>
|
||||
@@ -79,6 +89,8 @@ const form = reactive({
|
||||
smtpPassword: '',
|
||||
imapHost: '',
|
||||
imapPort: 993,
|
||||
imapUser: '',
|
||||
imapPassword: '',
|
||||
fromEmail: ''
|
||||
});
|
||||
const original = reactive({});
|
||||
@@ -94,6 +106,8 @@ const loadEmailSettings = async () => {
|
||||
form.smtpUser = s.smtp_user;
|
||||
form.imapHost = s.imap_host || '';
|
||||
form.imapPort = s.imap_port || 993;
|
||||
form.imapUser = s.imap_user || '';
|
||||
form.imapPassword = '';
|
||||
form.fromEmail = s.from_email;
|
||||
form.smtpPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
@@ -117,10 +131,13 @@ const saveEmailSettings = async () => {
|
||||
smtp_password: form.smtpPassword || undefined,
|
||||
imap_host: form.imapHost,
|
||||
imap_port: form.imapPort,
|
||||
imap_user: form.imapUser,
|
||||
imap_password: form.imapPassword || undefined,
|
||||
from_email: form.fromEmail
|
||||
});
|
||||
alert('Настройки Email сохранены');
|
||||
form.smtpPassword = '';
|
||||
form.imapPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
editMode.value = false;
|
||||
} catch (e) {
|
||||
@@ -131,6 +148,7 @@ const saveEmailSettings = async () => {
|
||||
const cancelEdit = () => {
|
||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||
form.smtpPassword = '';
|
||||
form.imapPassword = '';
|
||||
editMode.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -76,7 +76,7 @@ onMounted(async () => {
|
||||
|
||||
const saveTelegramSettings = async () => {
|
||||
try {
|
||||
await api.put('/telegram-settings', {
|
||||
await api.put('/settings/telegram-settings', {
|
||||
bot_token: form.botToken,
|
||||
bot_username: form.botUsername
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user