Addition of an AI chat assistant and a 100MB file upload limit.

This commit is contained in:
2026-09-30 16:30:41 +03:30
parent e94600a02e
commit ae21d9bbc7
8 changed files with 139 additions and 13 deletions
+85 -5
View File
@@ -35,13 +35,15 @@ import {
parseChatMessageText,
} from '@/lib/chatMessageFormat';
import { useAuthStore } from '@/stores/auth';
import AskMentoPanel from '@/components/AskMentoPanel.vue';
import UiIcon from '@/components/UiIcon.vue';
import UserAvatar from '@/components/UserAvatar.vue';
import type {
PaginatedResponse,
PersonDto,
PipelineBoardDto,
TicketDto,
import {
brand,
type PaginatedResponse,
type PersonDto,
type PipelineBoardDto,
type TicketDto,
} from '@mento/shared';
type PendingAttach = {
@@ -139,6 +141,8 @@ const crmLinkKind = ref<'person' | 'deal' | 'ticket'>('person');
const crmLinkQuery = ref('');
const crmLinkResults = ref<Array<{ id: string; title: string }>>([]);
const crmLinkBusy = ref(false);
const showAiAssist = ref(false);
const aiChatContext = ref('');
const chatSearchOpen = ref(false);
const chatSearchQuery = ref('');
const chatSearchIndex = ref(0);
@@ -774,6 +778,55 @@ function openCrmLinkPicker() {
void searchCrmLinks();
}
function messageSenderLabel(msg: ChatMessage) {
if (isMineMessage(msg)) return 'شما';
return (
peers.value.find((p) => p.chatUserId === msg.senderId)?.fullName ||
peers.value.find((p) => p.id === msg.senderId)?.fullName ||
'همکار'
);
}
function buildChatContext(): string {
const room = activeRoom.value;
const title = room ? roomTitle(room) : 'گفتگو';
const recent = messages.value.slice(-20);
const lines = recent.map((m) => {
const who = messageSenderLabel(m);
if (m.deleted) return `${who}: [حذف‌شده]`;
if (m.type === 'text') {
return `${who}: ${(m.content || '').replace(/\s+/g, ' ').trim().slice(0, 280)}`;
}
return `${who}: ${previewLabelForMessage(m)}`;
});
return `گفتگو: ${title}\n${lines.join('\n')}`.slice(0, 3500);
}
function openAiAssist() {
aiChatContext.value = buildChatContext();
showAiAssist.value = true;
emojiOpen.value = false;
}
function insertAiAnswer(text: string) {
const chunk = text.trim();
if (!chunk) return;
const el = draftInputEl.value;
const value = draft.value;
const start = el?.selectionStart ?? value.length;
const end = el?.selectionEnd ?? value.length;
const pad =
start > 0 && value.slice(0, start).length && !/\s$/.test(value.slice(0, start))
? '\n'
: '';
draft.value = value.slice(0, start) + pad + chunk + value.slice(end);
showAiAssist.value = false;
nextTick(() => {
draftInputEl.value?.focus();
onDraftInput();
});
}
function insertCrmEntity(item: { id: string; title: string }) {
const chunk = insertCrmLink(crmLinkKind.value, item.id, item.title);
const el = draftInputEl.value;
@@ -2264,6 +2317,14 @@ onUnmounted(() => {
<button type="button" class="fmt-btn" title="لینک CRM" @click="openCrmLinkPicker">
<UiIcon name="briefcase" :size="14" />
</button>
<button
type="button"
class="fmt-btn fmt-ai"
:title="brand.askLabelFa"
@click="openAiAssist"
>
<UiIcon name="sparkle" :size="14" />
</button>
</div>
<div class="composer-row">
<input
@@ -2704,6 +2765,22 @@ onUnmounted(() => {
</div>
</dialog>
<dialog v-if="showAiAssist" class="modal" open @click.self="showAiAssist = false">
<div class="modal-card wide ai-assist-sheet">
<h3>{{ brand.askLabelFa }}</h3>
<p class="hint">با توجه به CRM و چند پیام اخیر این گفتگو پاسخ می‌دهد؛ می‌توانید پاسخ را در پیش‌نویس درج کنید.</p>
<AskMentoPanel
:chat-context="aiChatContext"
allow-insert
:placeholder="`${brand.askLabelFa} درباره این گفتگو…`"
@insert-answer="insertAiAnswer"
/>
<div class="modal-actions">
<button type="button" @click="showAiAssist = false">بستن</button>
</div>
</div>
</dialog>
<dialog v-if="showMembers" class="modal" open @click.self="showMembers = false">
<div class="modal-card wide members-sheet">
<h3>اعضای {{ activeRoom ? roomTitle(activeRoom) : 'گفتگو' }}</h3>
@@ -3136,6 +3213,9 @@ onUnmounted(() => {
font-weight: 600;
font-size: 0.95rem;
}
.fmt-btn.fmt-ai {
color: #0f766e;
}
.msg-text em {
font-style: italic;
}