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
+29
View File
@@ -9,6 +9,14 @@ import { brand, type AskMentoResponse, type AskMentoSourceDto } from '@mento/sha
const props = defineProps<{
personId?: string;
placeholder?: string;
/** زمینهٔ گفتگو برای پاسخ مرتبط‌تر داخل چت */
chatContext?: string;
/** دکمهٔ درج پاسخ در پیش‌نویس پیام */
allowInsert?: boolean;
}>();
const emit = defineEmits<{
insertAnswer: [text: string];
}>();
const question = ref('');
@@ -33,6 +41,7 @@ async function submit() {
const { data } = await api.post<AskMentoResponse>('/ai/ask', {
question: q,
personId: props.personId || undefined,
chatContext: props.chatContext?.trim() || undefined,
});
result.value = data;
} catch (e) {
@@ -46,6 +55,12 @@ async function submit() {
asking.value = false;
}
}
function insertAnswer() {
const text = result.value?.answer?.trim();
if (!text) return;
emit('insertAnswer', text);
}
</script>
<template>
@@ -73,6 +88,14 @@ async function submit() {
{{ source.label }}
</RouterLink>
</div>
<button
v-if="allowInsert && result.answer.trim()"
type="button"
class="insert-btn"
@click="insertAnswer"
>
درج در پیام
</button>
</div>
</section>
</template>
@@ -153,6 +176,12 @@ button:disabled {
padding: 0.18rem 0.55rem;
}
.insert-btn {
margin-top: 0.65rem;
width: 100%;
background: #0f766e;
}
.error {
margin: 0;
color: #b91c1c;
+4 -3
View File
@@ -12,7 +12,8 @@ export type ChatTextSeg = {
const URL_RE =
/https?:\/\/[^\s<>"'`]+|(?:www\.)[^\s<>"'`]+/gi;
const CRM_RE = /mento:\/\/(person|deal|ticket)\/([A-Za-z0-9_-]+)/gi;
/** لینک عمیق CRM — طرح فعلی montact؛ mento برای پیام‌های قدیمی پشتیبانی می‌شود */
const CRM_RE = /(?:montact|mento):\/\/(person|deal|ticket)\/([A-Za-z0-9_-]+)/gi;
const BOLD_RE = /\*([^*\n]+)\*/g;
const ITALIC_RE = /_([^_\n]+)_/g;
const LIST_LINE_RE = /^\s*[-•]\s+/;
@@ -145,7 +146,7 @@ function applyHighlight(parts: ChatTextSeg[], q: string): ChatTextSeg[] {
}
/**
* فرمت سبک شبیه تلگرام: *پررنگ*، _ایتالیک_، لیست با - ، لینک http، mento://…
* فرمت سبک شبیه تلگرام: *پررنگ*، _ایتالیک_، لیست با - ، لینک http، montact://…
*/
export function parseChatMessageText(
content: string,
@@ -191,6 +192,6 @@ export function insertCrmLink(
id: string,
title?: string,
): string {
const link = `mento://${kind}/${id}`;
const link = `montact://${kind}/${id}`;
return title ? `${title} ${link}` : link;
}
+2 -2
View File
@@ -136,7 +136,7 @@ function shouldCountUnread(roomId: string, content: string, fromSelf: boolean):
}
const IMAGE_MAX_EDGE = 1600;
const IMAGE_WEBP_QUALITY = 0.82;
const MAX_UPLOAD_BYTES = 40 * 1024 * 1024;
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
/** Synced hard-delete marker (legacy edit attempts). */
const HARD_DELETE_MARKER = '__MENTO_DEL__';
/** Realtime hide command delivered as a normal text message (external WS delivers text reliably). */
@@ -1497,7 +1497,7 @@ export function useOrgChat() {
) {
if (!file) return;
if (file.size > MAX_UPLOAD_BYTES) {
throw new Error('حجم فایل بیش از حد مجاز است (۴۰ مگابایت)');
throw new Error('حجم فایل بیش از حد مجاز است (۱۰۰ مگابایت)');
}
const session = await ensureSession();
+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;
}