ваше сообщение коммита
This commit is contained in:
@@ -341,10 +341,20 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
try {
|
try {
|
||||||
// Найти или создать диалог
|
// Найти или создать диалог
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
const convResult = await db.getQuery()(
|
let convResult;
|
||||||
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
|
if (req.session.isAdmin) {
|
||||||
[conversationId, userId]
|
// Админ может писать в любой диалог
|
||||||
);
|
convResult = await db.getQuery()(
|
||||||
|
'SELECT * FROM conversations WHERE id = $1',
|
||||||
|
[conversationId]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Обычный пользователь — только в свой диалог
|
||||||
|
convResult = await db.getQuery()(
|
||||||
|
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
|
||||||
|
[conversationId, userId]
|
||||||
|
);
|
||||||
|
}
|
||||||
if (convResult.rows.length === 0) {
|
if (convResult.rows.length === 0) {
|
||||||
logger.warn('Conversation not found or access denied', { conversationId, userId });
|
logger.warn('Conversation not found or access denied', { conversationId, userId });
|
||||||
return res.status(404).json({ success: false, error: 'Диалог не найден или доступ запрещен' });
|
return res.status(404).json({ success: false, error: 'Диалог не найден или доступ запрещен' });
|
||||||
@@ -381,17 +391,29 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
const attachmentSize = file ? file.size : null;
|
const attachmentSize = file ? file.size : null;
|
||||||
const attachmentData = file ? file.buffer : null;
|
const attachmentData = file ? file.buffer : null;
|
||||||
|
|
||||||
// Сохраняем сообщение пользователя
|
// Определяем user_id для сообщения: всегда user_id диалога (контакта)
|
||||||
|
const recipientId = conversation.user_id;
|
||||||
|
// Определяем sender_type
|
||||||
|
let senderType = 'user';
|
||||||
|
let role = 'user';
|
||||||
|
if (req.session.isAdmin) {
|
||||||
|
senderType = 'admin';
|
||||||
|
role = 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем сообщение
|
||||||
const userMessageResult = await db.getQuery()(
|
const userMessageResult = 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,
|
||||||
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
|
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
|
||||||
VALUES ($1, $2, $3, 'user', 'user', 'web', $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, 'web', $6, $7, $8, $9)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
conversationId,
|
conversationId,
|
||||||
userId,
|
recipientId, // user_id контакта
|
||||||
messageContent,
|
messageContent,
|
||||||
|
senderType,
|
||||||
|
role,
|
||||||
attachmentFilename,
|
attachmentFilename,
|
||||||
attachmentMimetype,
|
attachmentMimetype,
|
||||||
attachmentSize,
|
attachmentSize,
|
||||||
@@ -403,7 +425,15 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
|
|
||||||
// Получаем ответ от ИИ, только если это было текстовое сообщение
|
// Получаем ответ от ИИ, только если это было текстовое сообщение
|
||||||
let aiMessage = null;
|
let aiMessage = null;
|
||||||
if (messageContent) { // Только для текстовых сообщений
|
// --- Новая логика автоответа ИИ ---
|
||||||
|
let shouldGenerateAiReply = true;
|
||||||
|
if (senderType === 'admin') {
|
||||||
|
// Если админ пишет не себе, не отвечаем
|
||||||
|
if (userId !== recipientId) {
|
||||||
|
shouldGenerateAiReply = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (messageContent && shouldGenerateAiReply) { // Только для текстовых сообщений и если разрешено
|
||||||
try {
|
try {
|
||||||
// Получаем настройки ассистента
|
// Получаем настройки ассистента
|
||||||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||||||
@@ -647,6 +677,46 @@ router.post('/process-guest', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/chat/ai-draft — генерация черновика ответа ИИ
|
||||||
|
router.post('/ai-draft', requireAuth, async (req, res) => {
|
||||||
|
const userId = req.session.userId;
|
||||||
|
const { conversationId, messages, language } = req.body;
|
||||||
|
if (!conversationId || !Array.isArray(messages) || messages.length === 0) {
|
||||||
|
return res.status(400).json({ success: false, error: 'conversationId и messages обязательны' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Получаем настройки ассистента
|
||||||
|
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||||||
|
let rules = null;
|
||||||
|
if (aiSettings && aiSettings.rules_id) {
|
||||||
|
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||||||
|
}
|
||||||
|
// Формируем prompt из выбранных сообщений
|
||||||
|
const promptText = messages.map(m => m.content).join('\n\n');
|
||||||
|
// Получаем последние 10 сообщений из диалога для истории
|
||||||
|
const historyResult = await db.getQuery()(
|
||||||
|
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 ORDER BY created_at DESC LIMIT 10',
|
||||||
|
[conversationId]
|
||||||
|
);
|
||||||
|
const history = historyResult.rows.reverse().map(msg => ({
|
||||||
|
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||||
|
content: msg.content
|
||||||
|
}));
|
||||||
|
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(promptText) : language;
|
||||||
|
const aiResponseContent = await aiAssistant.getResponse(
|
||||||
|
promptText,
|
||||||
|
detectedLanguage,
|
||||||
|
history,
|
||||||
|
aiSettings ? aiSettings.system_prompt : '',
|
||||||
|
rules ? rules.rules : null
|
||||||
|
);
|
||||||
|
res.json({ success: true, aiMessage: aiResponseContent });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error generating AI draft:', error);
|
||||||
|
res.status(500).json({ success: false, error: 'Ошибка генерации черновика' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Экспортируем маршрутизатор и функцию processGuestMessages отдельно
|
// Экспортируем маршрутизатор и функцию processGuestMessages отдельно
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
module.exports.processGuestMessages = processGuestMessages;
|
module.exports.processGuestMessages = processGuestMessages;
|
||||||
|
|||||||
@@ -6,9 +6,18 @@ const { broadcastMessagesUpdate } = require('../wsHub');
|
|||||||
// GET /api/messages?userId=123
|
// GET /api/messages?userId=123
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const userId = req.query.userId;
|
const userId = req.query.userId;
|
||||||
|
const conversationId = req.query.conversationId;
|
||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
if (userId) {
|
if (conversationId) {
|
||||||
|
result = await db.getQuery()(
|
||||||
|
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
|
||||||
|
FROM messages
|
||||||
|
WHERE conversation_id = $1
|
||||||
|
ORDER BY created_at ASC`,
|
||||||
|
[conversationId]
|
||||||
|
);
|
||||||
|
} else if (userId) {
|
||||||
result = await db.getQuery()(
|
result = await db.getQuery()(
|
||||||
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
|
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
|
||||||
FROM messages
|
FROM messages
|
||||||
@@ -96,4 +105,22 @@ router.get('/read-status', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/conversations?userId=123
|
||||||
|
router.get('/conversations', async (req, res) => {
|
||||||
|
const userId = req.query.userId;
|
||||||
|
if (!userId) return res.status(400).json({ error: 'userId required' });
|
||||||
|
try {
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Conversation not found' });
|
||||||
|
}
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: 'DB error', details: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-container" :style="{ '--chat-input-height': chatInputHeight + 'px' }">
|
<div class="chat-container" :style="{ '--chat-input-height': chatInputHeight + 'px' }">
|
||||||
<div ref="messagesContainer" class="chat-messages" @scroll="handleScroll">
|
<div ref="messagesContainer" class="chat-messages" @scroll="handleScroll">
|
||||||
<Message
|
<div v-for="message in messages" :key="message.id" :class="['message-wrapper', { 'selected-message': selectedMessageIds.includes(message.id) }]">
|
||||||
v-for="message in messages"
|
<template v-if="isAdmin">
|
||||||
:key="message.id"
|
<input type="checkbox" class="admin-select-checkbox" :checked="selectedMessageIds.includes(message.id)" @change="() => toggleSelectMessage(message.id)" />
|
||||||
:message="message"
|
</template>
|
||||||
/>
|
<Message :message="message" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="chatInputRef" class="chat-input">
|
<div ref="chatInputRef" class="chat-input">
|
||||||
@@ -68,6 +69,14 @@
|
|||||||
<path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" fill="currentColor"/>
|
<path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button v-if="props.isAdmin" class="chat-icon-btn ai-reply-btn" title="Сгенерировать ответ ИИ" @click="handleAiReply" :disabled="isAiLoading">
|
||||||
|
<template v-if="isAiLoading">
|
||||||
|
<svg class="ai-spinner" width="22" height="22" viewBox="0 0 50 50"><circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle></svg>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="8" r="4"/><path d="M8 16v2M16 16v2"/></svg>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="attachment-preview" v-if="localAttachments.length > 0">
|
<div class="attachment-preview" v-if="localAttachments.length > 0">
|
||||||
@@ -92,6 +101,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
||||||
import Message from './Message.vue';
|
import Message from './Message.vue';
|
||||||
|
import messagesService from '../services/messagesService.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
messages: {
|
messages: {
|
||||||
@@ -103,6 +113,7 @@ const props = defineProps({
|
|||||||
attachments: Array, // Для v-model
|
attachments: Array, // Для v-model
|
||||||
// Добавляем пропс для проверки, есть ли еще сообщения для загрузки
|
// Добавляем пропс для проверки, есть ли еще сообщения для загрузки
|
||||||
hasMoreMessages: Boolean,
|
hasMoreMessages: Boolean,
|
||||||
|
isAdmin: { type: Boolean, default: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -110,6 +121,7 @@ const emit = defineEmits([
|
|||||||
'update:attachments',
|
'update:attachments',
|
||||||
'send-message',
|
'send-message',
|
||||||
'load-more', // Событие для загрузки старых сообщений
|
'load-more', // Событие для загрузки старых сообщений
|
||||||
|
'ai-reply',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const messagesContainer = ref(null);
|
const messagesContainer = ref(null);
|
||||||
@@ -434,6 +446,45 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isAiLoading = ref(false);
|
||||||
|
|
||||||
|
const selectedMessageIds = ref([]);
|
||||||
|
|
||||||
|
function toggleSelectMessage(id) {
|
||||||
|
if (selectedMessageIds.value.includes(id)) {
|
||||||
|
selectedMessageIds.value = selectedMessageIds.value.filter(mid => mid !== id);
|
||||||
|
} else {
|
||||||
|
selectedMessageIds.value.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAiReply() {
|
||||||
|
if (isAiLoading.value) return;
|
||||||
|
// Если выбраны сообщения — отправляем их, иначе старое поведение
|
||||||
|
if (emit) {
|
||||||
|
const selectedMessages = props.messages.filter(m => selectedMessageIds.value.includes(m.id));
|
||||||
|
emit('ai-reply', selectedMessages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isAiLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await messagesService.sendMessage({
|
||||||
|
message: props.newMessage,
|
||||||
|
attachments: []
|
||||||
|
});
|
||||||
|
if (response && response.aiMessage && response.aiMessage.content) {
|
||||||
|
emit('update:newMessage', response.aiMessage.content);
|
||||||
|
} else {
|
||||||
|
emit('update:newMessage', '');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка генерации ответа ИИ:', e);
|
||||||
|
alert('Ошибка генерации ответа ИИ');
|
||||||
|
} finally {
|
||||||
|
isAiLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -689,4 +740,19 @@ onUnmounted(() => {
|
|||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-spinner {
|
||||||
|
animation: ai-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes ai-spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-message {
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
.admin-select-checkbox {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -6,6 +6,7 @@ const SettingsBlockchainView = () => import('../views/settings/BlockchainSetting
|
|||||||
const SettingsSecurityView = () => import('../views/settings/SecuritySettingsView.vue');
|
const SettingsSecurityView = () => import('../views/settings/SecuritySettingsView.vue');
|
||||||
const SettingsInterfaceView = () => import('../views/settings/InterfaceSettingsView.vue');
|
const SettingsInterfaceView = () => import('../views/settings/InterfaceSettingsView.vue');
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { setToStorage } from '../utils/storage.js';
|
||||||
|
|
||||||
console.log('router/index.js: Script loaded');
|
console.log('router/index.js: Script loaded');
|
||||||
|
|
||||||
@@ -152,4 +153,9 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
// Всегда закрываем сайдбар при переходе на любую страницу
|
||||||
|
setToStorage('showWalletSidebar', false);
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -5,8 +5,36 @@ export default {
|
|||||||
if (!userId) return [];
|
if (!userId) return [];
|
||||||
const { data } = await axios.get(`/api/messages?userId=${userId}`);
|
const { data } = await axios.get(`/api/messages?userId=${userId}`);
|
||||||
return data;
|
return data;
|
||||||
|
},
|
||||||
|
async sendMessage({ conversationId, message, attachments = [], toUserId }) {
|
||||||
|
const formData = new FormData();
|
||||||
|
if (conversationId) formData.append('conversationId', conversationId);
|
||||||
|
if (message) formData.append('message', message);
|
||||||
|
if (toUserId) formData.append('toUserId', toUserId);
|
||||||
|
attachments.forEach(file => {
|
||||||
|
formData.append('attachments', file);
|
||||||
|
});
|
||||||
|
const { data } = await axios.post('/api/chat/message', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
async getMessagesByConversationId(conversationId) {
|
||||||
|
if (!conversationId) return [];
|
||||||
|
const { data } = await axios.get(`/api/messages?conversationId=${conversationId}`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
async getConversationByUserId(userId) {
|
||||||
|
if (!userId) return null;
|
||||||
|
const { data } = await axios.get(`/api/messages/conversations?userId=${userId}`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
async generateAiDraft(conversationId, messages, language = 'auto') {
|
||||||
|
const { data } = await axios.post('/api/chat/ai-draft', { conversationId, messages, language });
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getAllMessages() {
|
export async function getAllMessages() {
|
||||||
const { data } = await axios.get('/api/messages');
|
const { data } = await axios.get('/api/messages');
|
||||||
|
|||||||
@@ -60,12 +60,18 @@
|
|||||||
<button class="delete-btn" @click="deleteContact">Удалить контакт</button>
|
<button class="delete-btn" @click="deleteContact">Удалить контакт</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="messages-block">
|
<div class="messages-block">
|
||||||
<h3>История сообщений</h3>
|
<h3>Чат с пользователем</h3>
|
||||||
<div v-if="isLoadingMessages" class="loading">Загрузка...</div>
|
<ChatInterface
|
||||||
<div v-else-if="messages.length === 0" class="empty">Нет сообщений</div>
|
:messages="messages"
|
||||||
<div v-else class="messages-list">
|
:isLoading="isLoadingMessages"
|
||||||
<Message v-for="msg in messages" :key="msg.id" :message="msg" />
|
:attachments="chatAttachments"
|
||||||
</div>
|
:newMessage="chatNewMessage"
|
||||||
|
:isAdmin="isAdmin"
|
||||||
|
@send-message="handleSendMessage"
|
||||||
|
@update:newMessage="val => chatNewMessage = val"
|
||||||
|
@update:attachments="val => chatAttachments = val"
|
||||||
|
@ai-reply="handleAiReply"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<el-dialog v-model="showTagModal" title="Добавить тег пользователю">
|
<el-dialog v-model="showTagModal" title="Добавить тег пользователю">
|
||||||
<div v-if="allTags.length">
|
<div v-if="allTags.length">
|
||||||
@@ -106,8 +112,10 @@ import { ref, computed, onMounted, watch } from 'vue';
|
|||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
import Message from '../../components/Message.vue';
|
import Message from '../../components/Message.vue';
|
||||||
|
import ChatInterface from '../../components/ChatInterface.vue';
|
||||||
import contactsService from '../../services/contactsService.js';
|
import contactsService from '../../services/contactsService.js';
|
||||||
import messagesService from '../../services/messagesService.js';
|
import messagesService from '../../services/messagesService.js';
|
||||||
|
import { useAuth } from '../../composables/useAuth';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -126,6 +134,11 @@ const showTagModal = ref(false);
|
|||||||
const newTagName = ref('');
|
const newTagName = ref('');
|
||||||
const newTagDescription = ref('');
|
const newTagDescription = ref('');
|
||||||
const messages = ref([]);
|
const messages = ref([]);
|
||||||
|
const chatAttachments = ref([]);
|
||||||
|
const chatNewMessage = ref('');
|
||||||
|
const { isAdmin } = useAuth();
|
||||||
|
const isAiLoading = ref(false);
|
||||||
|
const conversationId = ref(null);
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
isSidebarOpen.value = !isSidebarOpen.value;
|
isSidebarOpen.value = !isSidebarOpen.value;
|
||||||
@@ -207,10 +220,18 @@ async function loadMessages() {
|
|||||||
if (!contact.value || !contact.value.id) return;
|
if (!contact.value || !contact.value.id) return;
|
||||||
isLoadingMessages.value = true;
|
isLoadingMessages.value = true;
|
||||||
try {
|
try {
|
||||||
messages.value = await messagesService.getMessagesByUserId(contact.value.id);
|
// Получаем conversationId для контакта
|
||||||
if (messages.value.length > 0) {
|
const conv = await messagesService.getConversationByUserId(contact.value.id);
|
||||||
lastMessageDate.value = messages.value[messages.value.length - 1].created_at;
|
conversationId.value = conv?.id || null;
|
||||||
|
if (conversationId.value) {
|
||||||
|
messages.value = await messagesService.getMessagesByConversationId(conversationId.value);
|
||||||
|
if (messages.value.length > 0) {
|
||||||
|
lastMessageDate.value = messages.value[messages.value.length - 1].created_at;
|
||||||
|
} else {
|
||||||
|
lastMessageDate.value = null;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
messages.value = [];
|
||||||
lastMessageDate.value = null;
|
lastMessageDate.value = null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -296,6 +317,63 @@ function goBack() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSendMessage({ message, attachments }) {
|
||||||
|
console.log('handleSendMessage', message, attachments);
|
||||||
|
if (!contact.value || !contact.value.id || !conversationId.value) return;
|
||||||
|
const tempId = 'local-' + Date.now();
|
||||||
|
const optimisticMsg = {
|
||||||
|
id: tempId,
|
||||||
|
conversation_id: conversationId.value,
|
||||||
|
user_id: null,
|
||||||
|
content: message,
|
||||||
|
sender_type: 'user',
|
||||||
|
role: 'user',
|
||||||
|
channel: 'web',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
attachments: [],
|
||||||
|
isLocal: true
|
||||||
|
};
|
||||||
|
messages.value.push(optimisticMsg);
|
||||||
|
try {
|
||||||
|
await messagesService.sendMessage({
|
||||||
|
message,
|
||||||
|
conversationId: conversationId.value,
|
||||||
|
attachments,
|
||||||
|
toUserId: contact.value.id
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await loadMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAiReply(selectedMessages = []) {
|
||||||
|
console.log('[AI-ASSISTANT] Кнопка нажата, messages:', messages.value);
|
||||||
|
if (isAiLoading.value) {
|
||||||
|
console.log('[AI-ASSISTANT] Уже идёт генерация, выход');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(selectedMessages) || selectedMessages.length === 0) {
|
||||||
|
alert('Выберите хотя бы одно сообщение пользователя для генерации ответа.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isAiLoading.value = true;
|
||||||
|
try {
|
||||||
|
// Генерируем черновик ответа через новый endpoint
|
||||||
|
const draftResp = await messagesService.generateAiDraft(conversationId.value, selectedMessages);
|
||||||
|
if (draftResp && draftResp.success && draftResp.aiMessage) {
|
||||||
|
chatNewMessage.value = draftResp.aiMessage;
|
||||||
|
console.log('[AI-ASSISTANT] Черновик сгенерирован:', draftResp.aiMessage);
|
||||||
|
} else {
|
||||||
|
alert('Не удалось сгенерировать ответ ИИ.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка генерации ответа ИИ: ' + (e?.message || e));
|
||||||
|
} finally {
|
||||||
|
isAiLoading.value = false;
|
||||||
|
console.log('[AI-ASSISTANT] Генерация завершена');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await reloadContact();
|
await reloadContact();
|
||||||
await loadMessages();
|
await loadMessages();
|
||||||
|
|||||||
Reference in New Issue
Block a user