fix: share the dashboard assistant and count leads from data
The hero ask box now uses the same confirm-before-create path as the assistant, count questions use real filters, and lead mobile, Jalali follow-ups, and rial labels stay consistent. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
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';
|
||||
|
||||
export type CountPeriod = 'today' | 'yesterday' | 'week' | 'month';
|
||||
|
||||
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, ' ')
|
||||
.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';
|
||||
if (/معامله/.test(q)) return 'deal';
|
||||
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';
|
||||
if (/این\s*هفته|هفته\s*جاری/.test(q)) return 'week';
|
||||
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 = [
|
||||
'چند تا',
|
||||
'ته حساب',
|
||||
'مخاطب گوشی',
|
||||
'مخاطبین',
|
||||
'پیش فاکتور',
|
||||
'بایگانی',
|
||||
'مونتیفای',
|
||||
'معاملات',
|
||||
'مشتریان',
|
||||
'سرنخ',
|
||||
'لیدها',
|
||||
'فعالیت',
|
||||
'یادداشت',
|
||||
'بگویید',
|
||||
'داریم',
|
||||
'دارید',
|
||||
'هستند',
|
||||
'تعداد',
|
||||
'چقدر',
|
||||
'چنتا',
|
||||
'امروز',
|
||||
'دیروز',
|
||||
'جاری',
|
||||
'تبدیل',
|
||||
'برنده',
|
||||
'باخته',
|
||||
'ناراضی',
|
||||
'خوشحال',
|
||||
'محصول',
|
||||
'فاکتور',
|
||||
'معامله',
|
||||
'مشتری',
|
||||
'شرکت',
|
||||
'پروژه',
|
||||
'روتین',
|
||||
'رزرو',
|
||||
'نوبت',
|
||||
'تیکت',
|
||||
'منبع',
|
||||
'مرحله',
|
||||
'وضعیت',
|
||||
'هفته',
|
||||
'لطفا',
|
||||
'الان',
|
||||
'شده',
|
||||
'وارد',
|
||||
'تازه',
|
||||
'خاص',
|
||||
'بگو',
|
||||
'دارم',
|
||||
'هست',
|
||||
'است',
|
||||
'جاری',
|
||||
'فعال',
|
||||
'باز',
|
||||
'لید',
|
||||
'کالا',
|
||||
'چک',
|
||||
'ماه',
|
||||
'پیش',
|
||||
'های',
|
||||
'چند',
|
||||
'کل',
|
||||
'همه',
|
||||
'این',
|
||||
'یک',
|
||||
'یه',
|
||||
'از',
|
||||
'با',
|
||||
'در',
|
||||
'تا',
|
||||
'ها',
|
||||
].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;
|
||||
return {
|
||||
entity,
|
||||
personSource:
|
||||
entity === 'person' || entity === 'company' ? personSource : undefined,
|
||||
mood: entity === 'person' ? detectMood(q) : undefined,
|
||||
leadBucket: detectLeadBucket(q),
|
||||
dealBucket: detectDealBucket(q),
|
||||
period: detectPeriod(q),
|
||||
hint: leftoverHint(q, entity),
|
||||
};
|
||||
}
|
||||
|
||||
export function personSourceKeys(): readonly PersonImportSource[] {
|
||||
return PERSON_IMPORT_SOURCES;
|
||||
}
|
||||
Reference in New Issue
Block a user