2026-10-13 14:45:29 +03:30
|
|
|
|
import {
|
|
|
|
|
|
PERSON_IMPORT_SOURCES,
|
|
|
|
|
|
type PersonImportSource,
|
|
|
|
|
|
} from '@mento/shared';
|
|
|
|
|
|
|
|
|
|
|
|
export type CountEntity =
|
|
|
|
|
|
| 'lead'
|
|
|
|
|
|
| 'person'
|
|
|
|
|
|
| 'company'
|
|
|
|
|
|
| 'deal'
|
|
|
|
|
|
| 'invoice'
|
|
|
|
|
|
| 'quote'
|
|
|
|
|
|
| 'product'
|
|
|
|
|
|
| 'activity'
|
|
|
|
|
|
| 'ticket'
|
|
|
|
|
|
| 'note'
|
|
|
|
|
|
| 'cheque'
|
|
|
|
|
|
| 'reservation'
|
|
|
|
|
|
| 'routine'
|
|
|
|
|
|
| 'project';
|
|
|
|
|
|
|
2026-10-15 10:48:30 +03:30
|
|
|
|
export type CountPeriod = 'today' | 'yesterday' | 'week' | 'last_week' | 'month' | 'last_month';
|
2026-10-13 14:45:29 +03:30
|
|
|
|
|
|
|
|
|
|
export type LeadBucket = 'summary' | 'open' | 'archived' | 'converted';
|
|
|
|
|
|
export type DealBucket = 'all' | 'open' | 'won' | 'lost' | 'stale';
|
|
|
|
|
|
|
|
|
|
|
|
export type CountIntent = {
|
|
|
|
|
|
entity: CountEntity;
|
|
|
|
|
|
personSource?: PersonImportSource;
|
|
|
|
|
|
mood?: 'newcomer' | 'happy' | 'unhappy' | 'special';
|
|
|
|
|
|
leadBucket: LeadBucket;
|
|
|
|
|
|
dealBucket: DealBucket;
|
|
|
|
|
|
period?: CountPeriod;
|
|
|
|
|
|
/** متن باقیمانده برای تطبیق مرحله یا منبع لید */
|
|
|
|
|
|
hint: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const SOURCE_PATTERNS: Array<{ re: RegExp; source: PersonImportSource }> = [
|
|
|
|
|
|
{ re: /دستی/, source: 'manual' },
|
|
|
|
|
|
{ re: /اکسل/, source: 'excel' },
|
|
|
|
|
|
{ re: /دیدار/, source: 'didar' },
|
|
|
|
|
|
{ re: /ته\s*حساب/, source: 'tahesab' },
|
|
|
|
|
|
{ re: /گوشی|مخاطب\s*گوشی|مخاطبین/, source: 'phone' },
|
|
|
|
|
|
{ re: /فرم/, source: 'lead_form' },
|
|
|
|
|
|
{ re: /مونتیفای/, source: 'montify' },
|
|
|
|
|
|
{ re: /سایر|بقیه/, source: 'other' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
export function normalizeAskText(raw: string) {
|
|
|
|
|
|
return raw
|
|
|
|
|
|
.replace(/ي/g, 'ی')
|
|
|
|
|
|
.replace(/ك/g, 'ک')
|
|
|
|
|
|
.replace(//g, ' ')
|
2026-10-15 10:48:30 +03:30
|
|
|
|
.replace(/[؟?!.,،؛:]+/g, ' ')
|
2026-10-13 14:45:29 +03:30
|
|
|
|
.replace(/\s+/g, ' ')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.toLowerCase();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function isCountQuestion(q: string) {
|
|
|
|
|
|
return /چند|تعداد|چقدر|چنتا|چند\s*تا/.test(q);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function detectEntity(q: string): CountEntity | null {
|
|
|
|
|
|
if (/لید|سرنخ/.test(q)) return 'lead';
|
|
|
|
|
|
if (/پیش\s*فاکتور/.test(q)) return 'quote';
|
|
|
|
|
|
if (/فاکتور/.test(q)) return 'invoice';
|
2026-10-15 10:48:30 +03:30
|
|
|
|
if (/معاملات|معامله/.test(q)) return 'deal';
|
2026-10-13 14:45:29 +03:30
|
|
|
|
if (/چک/.test(q)) return 'cheque';
|
|
|
|
|
|
if (/رزرو|نوبت/.test(q)) return 'reservation';
|
|
|
|
|
|
if (/روتین/.test(q)) return 'routine';
|
|
|
|
|
|
if (/پروژه/.test(q)) return 'project';
|
|
|
|
|
|
if (/تیکت/.test(q)) return 'ticket';
|
|
|
|
|
|
if (/یادداشت/.test(q)) return 'note';
|
|
|
|
|
|
if (/فعالیت/.test(q)) return 'activity';
|
|
|
|
|
|
if (/محصول|کالا/.test(q)) return 'product';
|
|
|
|
|
|
if (/شرکت/.test(q) && !/مشتری/.test(q)) return 'company';
|
|
|
|
|
|
if (/مشتری|اشخاص|شخص ها|افراد|مخاطب/.test(q)) return 'person';
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function detectPeriod(q: string): CountPeriod | undefined {
|
|
|
|
|
|
if (/دیروز/.test(q)) return 'yesterday';
|
|
|
|
|
|
if (/امروز/.test(q)) return 'today';
|
2026-10-15 10:48:30 +03:30
|
|
|
|
if (/هفته\s*(?:ی\s+)?(گذشته|قبل|پیش)/.test(q)) return 'last_week';
|
2026-10-13 14:45:29 +03:30
|
|
|
|
if (/این\s*هفته|هفته\s*جاری/.test(q)) return 'week';
|
2026-10-15 10:48:30 +03:30
|
|
|
|
if (/ماه\s*(?:ی\s+)?(گذشته|قبل|پیش)/.test(q)) return 'last_month';
|
2026-10-13 14:45:29 +03:30
|
|
|
|
if (/این\s*ماه|ماه\s*جاری/.test(q)) return 'month';
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function detectLeadBucket(q: string): LeadBucket {
|
|
|
|
|
|
if (/بایگانی/.test(q)) return 'archived';
|
|
|
|
|
|
if (/تبدیل/.test(q)) return 'converted';
|
|
|
|
|
|
if (/باز|فعال|جاری/.test(q)) return 'open';
|
|
|
|
|
|
return 'summary';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function detectDealBucket(q: string): DealBucket {
|
|
|
|
|
|
if (/در\s*خطر|راکد|گندیده/.test(q)) return 'stale';
|
|
|
|
|
|
if (/برنده|موفق|بسته\s*شده\s*موفق/.test(q)) return 'won';
|
|
|
|
|
|
if (/باخته|از\s*دست\s*رفته|ناموفق/.test(q)) return 'lost';
|
|
|
|
|
|
if (/باز|جاری|فعال/.test(q)) return 'open';
|
|
|
|
|
|
return 'all';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function detectMood(q: string): CountIntent['mood'] {
|
|
|
|
|
|
if (/ناراضی/.test(q)) return 'unhappy';
|
|
|
|
|
|
if (/خوشحال/.test(q)) return 'happy';
|
|
|
|
|
|
if (/خاص/.test(q)) return 'special';
|
|
|
|
|
|
if (/تازه/.test(q)) return 'newcomer';
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const HINT_STOP = [
|
|
|
|
|
|
'چند تا',
|
|
|
|
|
|
'ته حساب',
|
|
|
|
|
|
'مخاطب گوشی',
|
|
|
|
|
|
'مخاطبین',
|
|
|
|
|
|
'پیش فاکتور',
|
|
|
|
|
|
'بایگانی',
|
|
|
|
|
|
'مونتیفای',
|
|
|
|
|
|
'معاملات',
|
|
|
|
|
|
'مشتریان',
|
|
|
|
|
|
'سرنخ',
|
|
|
|
|
|
'لیدها',
|
|
|
|
|
|
'فعالیت',
|
|
|
|
|
|
'یادداشت',
|
|
|
|
|
|
'بگویید',
|
|
|
|
|
|
'داریم',
|
|
|
|
|
|
'دارید',
|
|
|
|
|
|
'هستند',
|
|
|
|
|
|
'تعداد',
|
|
|
|
|
|
'چقدر',
|
|
|
|
|
|
'چنتا',
|
|
|
|
|
|
'امروز',
|
|
|
|
|
|
'دیروز',
|
|
|
|
|
|
'جاری',
|
|
|
|
|
|
'تبدیل',
|
|
|
|
|
|
'برنده',
|
|
|
|
|
|
'باخته',
|
|
|
|
|
|
'ناراضی',
|
|
|
|
|
|
'خوشحال',
|
|
|
|
|
|
'محصول',
|
|
|
|
|
|
'فاکتور',
|
|
|
|
|
|
'معامله',
|
|
|
|
|
|
'مشتری',
|
|
|
|
|
|
'شرکت',
|
|
|
|
|
|
'پروژه',
|
|
|
|
|
|
'روتین',
|
|
|
|
|
|
'رزرو',
|
|
|
|
|
|
'نوبت',
|
|
|
|
|
|
'تیکت',
|
|
|
|
|
|
'منبع',
|
|
|
|
|
|
'مرحله',
|
|
|
|
|
|
'وضعیت',
|
|
|
|
|
|
'هفته',
|
2026-10-15 10:48:30 +03:30
|
|
|
|
'گذشته',
|
|
|
|
|
|
'پیش',
|
|
|
|
|
|
'قبل',
|
2026-10-13 14:45:29 +03:30
|
|
|
|
'لطفا',
|
|
|
|
|
|
'الان',
|
|
|
|
|
|
'شده',
|
|
|
|
|
|
'وارد',
|
|
|
|
|
|
'تازه',
|
|
|
|
|
|
'خاص',
|
|
|
|
|
|
'بگو',
|
|
|
|
|
|
'دارم',
|
|
|
|
|
|
'هست',
|
|
|
|
|
|
'است',
|
|
|
|
|
|
'جاری',
|
|
|
|
|
|
'فعال',
|
|
|
|
|
|
'باز',
|
|
|
|
|
|
'لید',
|
|
|
|
|
|
'کالا',
|
|
|
|
|
|
'چک',
|
|
|
|
|
|
'ماه',
|
|
|
|
|
|
'پیش',
|
|
|
|
|
|
'های',
|
|
|
|
|
|
'چند',
|
|
|
|
|
|
'کل',
|
|
|
|
|
|
'همه',
|
|
|
|
|
|
'این',
|
|
|
|
|
|
'یک',
|
|
|
|
|
|
'یه',
|
|
|
|
|
|
'از',
|
|
|
|
|
|
'با',
|
|
|
|
|
|
'در',
|
|
|
|
|
|
'تا',
|
|
|
|
|
|
'ها',
|
|
|
|
|
|
].sort((a, b) => b.length - a.length);
|
|
|
|
|
|
|
|
|
|
|
|
const PERSON_HINT_STOP = [
|
|
|
|
|
|
'ته حساب',
|
|
|
|
|
|
'مخاطب گوشی',
|
|
|
|
|
|
'مخاطبین',
|
|
|
|
|
|
'مونتیفای',
|
|
|
|
|
|
'ناراضی',
|
|
|
|
|
|
'خوشحال',
|
|
|
|
|
|
'دستی',
|
|
|
|
|
|
'اکسل',
|
|
|
|
|
|
'دیدار',
|
|
|
|
|
|
'گوشی',
|
|
|
|
|
|
'مخاطب',
|
|
|
|
|
|
'فرم',
|
|
|
|
|
|
'سایر',
|
|
|
|
|
|
'تازه',
|
|
|
|
|
|
'خاص',
|
|
|
|
|
|
].sort((a, b) => b.length - a.length);
|
|
|
|
|
|
|
|
|
|
|
|
function leftoverHint(q: string, entity: CountEntity) {
|
|
|
|
|
|
let text = ` ${q} `;
|
|
|
|
|
|
const words = entity === 'person' || entity === 'company'
|
|
|
|
|
|
? [...HINT_STOP, ...PERSON_HINT_STOP]
|
|
|
|
|
|
: HINT_STOP;
|
|
|
|
|
|
for (const word of [...words].sort((a, b) => b.length - a.length)) {
|
|
|
|
|
|
text = text.replaceAll(word, ' ');
|
|
|
|
|
|
}
|
|
|
|
|
|
return text.replace(/\s+/g, ' ').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function classifyCountQuestion(raw: string): CountIntent | null {
|
|
|
|
|
|
const q = normalizeAskText(raw);
|
|
|
|
|
|
if (!isCountQuestion(q)) return null;
|
|
|
|
|
|
const entity = detectEntity(q);
|
|
|
|
|
|
if (!entity) return null;
|
|
|
|
|
|
const personSource = SOURCE_PATTERNS.find((item) => item.re.test(q))?.source;
|
2026-10-15 10:48:30 +03:30
|
|
|
|
const period = detectPeriod(q);
|
|
|
|
|
|
let hint = leftoverHint(q, entity);
|
|
|
|
|
|
if (period && entity === 'lead' && !/مرحله|وضعیت|ستون/.test(q)) {
|
|
|
|
|
|
hint = hint.replace(/(^|\s)جدید(?=\s|$)/g, ' ').replace(/\s+/g, ' ').trim();
|
|
|
|
|
|
}
|
2026-10-13 14:45:29 +03:30
|
|
|
|
return {
|
|
|
|
|
|
entity,
|
|
|
|
|
|
personSource:
|
|
|
|
|
|
entity === 'person' || entity === 'company' ? personSource : undefined,
|
|
|
|
|
|
mood: entity === 'person' ? detectMood(q) : undefined,
|
|
|
|
|
|
leadBucket: detectLeadBucket(q),
|
|
|
|
|
|
dealBucket: detectDealBucket(q),
|
2026-10-15 10:48:30 +03:30
|
|
|
|
period,
|
|
|
|
|
|
hint,
|
2026-10-13 14:45:29 +03:30
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function personSourceKeys(): readonly PersonImportSource[] {
|
|
|
|
|
|
return PERSON_IMPORT_SOURCES;
|
|
|
|
|
|
}
|