ваше сообщение коммита
This commit is contained in:
@@ -144,6 +144,7 @@ import contactsService from '../../services/contactsService.js';
|
||||
import messagesService from '../../services/messagesService.js';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import tablesService from '../../services/tablesService';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -168,6 +169,59 @@ const { isAdmin } = useAuthContext();
|
||||
const isAiLoading = ref(false);
|
||||
const conversationId = ref(null);
|
||||
|
||||
// id таблицы тегов (будет найден или создан)
|
||||
const tagsTableId = ref(null);
|
||||
|
||||
async function ensureTagsTable() {
|
||||
// Получаем все пользовательские таблицы
|
||||
const tables = await tablesService.getTables();
|
||||
let tagsTable = tables.find(t => t.name === 'Теги клиентов');
|
||||
if (!tagsTable) {
|
||||
// Если таблицы нет — создаём
|
||||
tagsTable = await tablesService.createTable({
|
||||
name: 'Теги клиентов',
|
||||
description: 'Справочник тегов для контактов',
|
||||
isRagSourceId: 2 // не источник для RAG по умолчанию
|
||||
});
|
||||
// Добавляем столбцы
|
||||
await tablesService.addColumn(tagsTable.id, { name: 'Название', type: 'text' });
|
||||
await tablesService.addColumn(tagsTable.id, { name: 'Описание', type: 'text' });
|
||||
} else {
|
||||
// Проверяем, есть ли нужные столбцы, если таблица уже была создана
|
||||
const table = await tablesService.getTable(tagsTable.id);
|
||||
const hasName = table.columns.some(col => col.name === 'Название');
|
||||
const hasDesc = table.columns.some(col => col.name === 'Описание');
|
||||
if (!hasName) await tablesService.addColumn(tagsTable.id, { name: 'Название', type: 'text' });
|
||||
if (!hasDesc) await tablesService.addColumn(tagsTable.id, { name: 'Описание', type: 'text' });
|
||||
}
|
||||
tagsTableId.value = tagsTable.id;
|
||||
return tagsTable.id;
|
||||
}
|
||||
|
||||
async function loadAllTags() {
|
||||
// Убедимся, что таблица тегов есть
|
||||
const tableId = await ensureTagsTable();
|
||||
// Загружаем все строки из таблицы тегов
|
||||
const table = await tablesService.getTable(tableId);
|
||||
// Ожидаем, что первый столбец — название тега, второй — описание (если есть)
|
||||
const nameCol = table.columns[0];
|
||||
const descCol = table.columns[1];
|
||||
allTags.value = table.rows.map(row => {
|
||||
const nameCell = table.cellValues.find(c => c.row_id === row.id && c.column_id === nameCol.id);
|
||||
const descCell = descCol ? table.cellValues.find(c => c.row_id === row.id && c.column_id === descCol.id) : null;
|
||||
return {
|
||||
id: row.id,
|
||||
name: nameCell ? nameCell.value : '',
|
||||
description: descCell ? descCell.value : ''
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function openTagModal() {
|
||||
showTagModal.value = true;
|
||||
loadAllTags();
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
isSidebarOpen.value = !isSidebarOpen.value;
|
||||
}
|
||||
@@ -270,58 +324,6 @@ async function loadMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserTags() {
|
||||
if (!contact.value) return;
|
||||
const res = await fetch(`/api/users/${contact.value.id}/tags`);
|
||||
userTags.value = await res.json();
|
||||
selectedTags.value = userTags.value.map(t => t.id);
|
||||
}
|
||||
|
||||
async function openTagModal() {
|
||||
await fetch('/api/tags/init', { method: 'POST' })
|
||||
const res = await fetch('/api/tags')
|
||||
allTags.value = await res.json()
|
||||
await loadUserTags()
|
||||
showTagModal.value = true
|
||||
}
|
||||
|
||||
async function createTag() {
|
||||
const res = await fetch('/api/tags', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newTagName.value, description: newTagDescription.value })
|
||||
});
|
||||
const newTag = await res.json();
|
||||
await fetch(`/api/users/${contact.value.id}/tags`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tag_id: newTag.id })
|
||||
});
|
||||
const tagsRes = await fetch('/api/tags');
|
||||
allTags.value = await tagsRes.json();
|
||||
await loadUserTags();
|
||||
newTagName.value = '';
|
||||
newTagDescription.value = '';
|
||||
}
|
||||
|
||||
async function addTagsToUser() {
|
||||
for (const tagId of selectedTags.value) {
|
||||
await fetch(`/api/users/${contact.value.id}/tags`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tag_id: tagId })
|
||||
})
|
||||
}
|
||||
await loadUserTags()
|
||||
}
|
||||
|
||||
async function removeUserTag(tagId) {
|
||||
await fetch(`/api/users/${contact.value.id}/tags/${tagId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
await loadUserTags();
|
||||
}
|
||||
|
||||
async function reloadContact() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
@@ -454,15 +456,99 @@ async function unblockUser() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Теги ---
|
||||
async function createTag() {
|
||||
if (!newTagName.value) return;
|
||||
const tableId = await ensureTagsTable();
|
||||
const table = await tablesService.getTable(tableId);
|
||||
const nameCol = table.columns[0];
|
||||
const descCol = table.columns[1];
|
||||
// 1. Создаём строку
|
||||
const newRow = await tablesService.addRow(tableId);
|
||||
console.log('DEBUG newRow:', newRow);
|
||||
if (!newRow || !newRow.id) {
|
||||
console.error('Ошибка: не удалось получить id новой строки', newRow);
|
||||
alert('Ошибка: не удалось получить id новой строки. См. консоль.');
|
||||
return;
|
||||
}
|
||||
const newRowId = newRow.id;
|
||||
// 2. Сохраняем имя
|
||||
await tablesService.saveCell({
|
||||
table_id: tableId,
|
||||
row_id: newRowId,
|
||||
column_id: nameCol.id,
|
||||
value: newTagName.value
|
||||
});
|
||||
// 3. Сохраняем описание (если есть столбец)
|
||||
if (descCol && newTagDescription.value) {
|
||||
await tablesService.saveCell({
|
||||
table_id: tableId,
|
||||
row_id: newRowId,
|
||||
column_id: descCol.id,
|
||||
value: newTagDescription.value
|
||||
});
|
||||
}
|
||||
// 4. Обновляем список тегов
|
||||
await loadAllTags();
|
||||
// 5. Автоматически выбираем новый тег для пользователя
|
||||
selectedTags.value = [...selectedTags.value, newRowId];
|
||||
await addTagsToUser();
|
||||
// 6. Очищаем поля
|
||||
newTagName.value = '';
|
||||
newTagDescription.value = '';
|
||||
}
|
||||
|
||||
async function loadUserTags() {
|
||||
if (!contact.value || !contact.value.id) {
|
||||
userTags.value = [];
|
||||
return;
|
||||
}
|
||||
// Получаем id тегов пользователя
|
||||
const tagIds = await contactsService.getContactTags(contact.value.id);
|
||||
if (!Array.isArray(tagIds) || tagIds.length === 0) {
|
||||
userTags.value = [];
|
||||
return;
|
||||
}
|
||||
// Загружаем справочник тегов
|
||||
await loadAllTags();
|
||||
// Сопоставляем id с объектами тегов
|
||||
userTags.value = allTags.value.filter(tag => tagIds.includes(tag.id));
|
||||
}
|
||||
|
||||
// После добавления/удаления тегов всегда обновляем userTags
|
||||
async function addTagsToUser() {
|
||||
if (!contact.value || !contact.value.id) return;
|
||||
if (!selectedTags.value || selectedTags.value.length === 0) return;
|
||||
try {
|
||||
await contactsService.addTagsToContact(contact.value.id, selectedTags.value);
|
||||
await loadUserTags();
|
||||
showTagModal.value = false;
|
||||
ElMessageBox.alert('Теги успешно добавлены.', 'Успех', { type: 'success' });
|
||||
} catch (e) {
|
||||
ElMessageBox.alert('Ошибка добавления тегов: ' + (e?.response?.data?.error || e?.message || e), 'Ошибка', { type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
async function removeUserTag(tagId) {
|
||||
if (!contact.value || !contact.value.id) return;
|
||||
try {
|
||||
await contactsService.removeTagFromContact(contact.value.id, tagId);
|
||||
await loadUserTags();
|
||||
ElMessageBox.alert('Тег успешно удален.', 'Успех', { type: 'success' });
|
||||
} catch (e) {
|
||||
ElMessageBox.alert('Ошибка удаления тега: ' + (e?.response?.data?.error || e?.message || e), 'Ошибка', { type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await reloadContact();
|
||||
await loadMessages();
|
||||
await loadUserTags();
|
||||
await loadMessages();
|
||||
});
|
||||
watch(userId, async () => {
|
||||
await reloadContact();
|
||||
await loadMessages();
|
||||
await loadUserTags();
|
||||
await loadMessages();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,6 +7,43 @@
|
||||
<form @submit.prevent="saveSettings">
|
||||
<label>Системный промт</label>
|
||||
<textarea v-model="settings.system_prompt" rows="3" />
|
||||
<!-- Блок плейсхолдеров -->
|
||||
<div class="placeholders-block">
|
||||
<h4>Плейсхолдеры пользовательских таблиц</h4>
|
||||
<div v-if="placeholders.length === 0" class="empty-placeholder">Нет пользовательских плейсхолдеров</div>
|
||||
<table v-else class="placeholders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Плейсхолдер</th>
|
||||
<th>Столбец</th>
|
||||
<th>Таблица</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="ph in placeholders" :key="ph.column_id">
|
||||
<td><code>{ {{ ph.placeholder }} }</code></td>
|
||||
<td>{{ ph.column_name }}</td>
|
||||
<td>{{ ph.table_name }}</td>
|
||||
<td><button type="button" @click="openEditPlaceholder(ph)">Редактировать</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Модалка редактирования плейсхолдера -->
|
||||
<div v-if="editingPlaceholder" class="modal-bg">
|
||||
<div class="modal">
|
||||
<h4>Редактировать плейсхолдер</h4>
|
||||
<div><b>Таблица:</b> {{ editingPlaceholder.table_name }}</div>
|
||||
<div><b>Столбец:</b> {{ editingPlaceholder.column_name }}</div>
|
||||
<label>Плейсхолдер</label>
|
||||
<input v-model="editingPlaceholderValue" />
|
||||
<div class="actions">
|
||||
<button type="button" @click="savePlaceholderEdit">Сохранить</button>
|
||||
<button type="button" @click="closeEditPlaceholder">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -86,6 +123,10 @@ const filteredEmbeddingModels = computed(() => {
|
||||
if (!selectedLLM.value) return embeddingModels.value;
|
||||
return embeddingModels.value.filter(m => m.provider === selectedLLM.value.provider);
|
||||
});
|
||||
const placeholders = ref([]);
|
||||
const editingPlaceholder = ref(null);
|
||||
const editingPlaceholderValue = ref('');
|
||||
|
||||
async function loadUserTables() {
|
||||
const { data } = await axios.get('/tables');
|
||||
userTables.value = Array.isArray(data) ? data : [];
|
||||
@@ -116,6 +157,24 @@ async function loadEmbeddingModels() {
|
||||
const { data } = await axios.get('/settings/embedding-models');
|
||||
embeddingModels.value = data.models || [];
|
||||
}
|
||||
async function loadPlaceholders() {
|
||||
const { data } = await axios.get('/tables/placeholders/all');
|
||||
placeholders.value = Array.isArray(data) ? data : [];
|
||||
}
|
||||
function openEditPlaceholder(ph) {
|
||||
editingPlaceholder.value = { ...ph };
|
||||
editingPlaceholderValue.value = ph.placeholder;
|
||||
}
|
||||
function closeEditPlaceholder() {
|
||||
editingPlaceholder.value = null;
|
||||
editingPlaceholderValue.value = '';
|
||||
}
|
||||
async function savePlaceholderEdit() {
|
||||
if (!editingPlaceholder.value) return;
|
||||
await axios.patch(`/tables/column/${editingPlaceholder.value.column_id}`, { placeholder: editingPlaceholderValue.value });
|
||||
await loadPlaceholders();
|
||||
closeEditPlaceholder();
|
||||
}
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
loadUserTables();
|
||||
@@ -124,6 +183,7 @@ onMounted(() => {
|
||||
loadEmailList();
|
||||
loadLLMModels();
|
||||
loadEmbeddingModels();
|
||||
loadPlaceholders();
|
||||
});
|
||||
async function saveSettings() {
|
||||
await axios.put('/settings/ai-assistant', settings.value);
|
||||
@@ -275,4 +335,30 @@ button[type="button"] {
|
||||
.rag-table-link:hover {
|
||||
color: #27ae38;
|
||||
}
|
||||
.placeholders-block {
|
||||
margin: 1.5em 0 2em 0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1em 1.5em;
|
||||
}
|
||||
.placeholders-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 0.5em;
|
||||
background: #fff;
|
||||
}
|
||||
.placeholders-table th, .placeholders-table td {
|
||||
border: 1px solid #ececec;
|
||||
padding: 0.5em 0.7em;
|
||||
font-size: 1em;
|
||||
}
|
||||
.placeholders-table th {
|
||||
background: #f7f7f7;
|
||||
font-weight: 600;
|
||||
}
|
||||
.empty-placeholder {
|
||||
color: #888;
|
||||
font-size: 1em;
|
||||
margin: 0.7em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -24,7 +24,7 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import axios from 'axios';
|
||||
import tablesService from '@/services/tablesService';
|
||||
const $route = useRoute();
|
||||
const router = useRouter();
|
||||
const name = ref('');
|
||||
@@ -32,14 +32,14 @@ const description = ref('');
|
||||
const isRagSourceId = ref(2);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await axios.get(`/tables/${$route.params.id}`);
|
||||
const data = await tablesService.getTable($route.params.id);
|
||||
name.value = data.name;
|
||||
description.value = data.description;
|
||||
isRagSourceId.value = data.is_rag_source_id || 2;
|
||||
});
|
||||
|
||||
async function save() {
|
||||
await axios.patch(`/api/tables/${$route.params.id}`, {
|
||||
await tablesService.updateTable($route.params.id, {
|
||||
name: name.value,
|
||||
description: description.value,
|
||||
isRagSourceId: isRagSourceId.value
|
||||
|
||||
@@ -14,7 +14,6 @@ import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import UserTablesList from '../../components/tables/UserTablesList.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
// import TagsTableView from '../../components/tables/TagsTableView.vue'; // больше не используется
|
||||
const router = useRouter();
|
||||
const { isAdmin } = useAuthContext();
|
||||
function goBack() {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<TagsTableView />
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import TagsTableView from '../../components/tables/TagsTableView.vue';
|
||||
</script>
|
||||
Reference in New Issue
Block a user