ваше сообщение коммита

This commit is contained in:
2025-07-11 16:45:09 +03:00
parent e0ec889863
commit 584ff401ad
42 changed files with 1945 additions and 1004 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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() {

View File

@@ -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>