From fa1231df37d22a2468cd564939161f96d6e99c08 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Sat, 21 Feb 2026 16:27:09 +0700 Subject: [PATCH] feat: add scoped context payload and rollbackable document changes --- frontend/app.vue | 246 ++++++++++++++++++++- frontend/server/agent/crmAgent.ts | 26 +++ frontend/server/agent/langgraphCrmAgent.ts | 219 +++++++++++++++++- frontend/server/api/pilot-chat.post.ts | 68 +++++- frontend/server/utils/changeSet.ts | 132 ++++++++++- 5 files changed, 678 insertions(+), 13 deletions(-) diff --git a/frontend/app.vue b/frontend/app.vue index a7e8275..77c4afc 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -337,6 +337,31 @@ type PilotMessage = { }; type PilotChangeItem = NonNullable[number]; +type ContextScope = "summary" | "deal" | "message" | "calendar"; +type PilotContextPayload = { + scopes: ContextScope[]; + summary?: { + contactId: string; + name: string; + }; + deal?: { + dealId: string; + title: string; + contact: string; + }; + message?: { + contactId?: string; + contact?: string; + intent: "add_message_or_reminder"; + }; + calendar?: { + view: CalendarView; + period: string; + selectedDateKey: string; + focusedEventId?: string; + eventIds: string[]; + }; +}; type ChatConversation = { id: string; @@ -357,6 +382,8 @@ const pilotMicError = ref(null); const pilotWaveContainer = ref(null); const livePilotUserText = ref(""); const livePilotAssistantText = ref(""); +const contextPickerEnabled = ref(false); +const contextScopes = ref([]); const pilotLiveLogs = ref>([]); const PILOT_LIVE_LOGS_PREVIEW_LIMIT = 5; const pilotLiveLogsExpanded = ref(false); @@ -374,6 +401,27 @@ const pilotVisibleLogCount = computed(() => function togglePilotLiveLogsExpanded() { pilotLiveLogsExpanded.value = !pilotLiveLogsExpanded.value; } + +function toggleContextPicker() { + contextPickerEnabled.value = !contextPickerEnabled.value; +} + +function hasContextScope(scope: ContextScope) { + return contextScopes.value.includes(scope); +} + +function toggleContextScope(scope: ContextScope) { + if (!contextPickerEnabled.value) return; + if (hasContextScope(scope)) { + contextScopes.value = contextScopes.value.filter((item) => item !== scope); + return; + } + contextScopes.value = [...contextScopes.value, scope]; +} + +function removeContextScope(scope: ContextScope) { + contextScopes.value = contextScopes.value.filter((item) => item !== scope); +} let pilotMediaRecorder: MediaRecorder | null = null; let pilotRecorderStream: MediaStream | null = null; let pilotRecordingChunks: Blob[] = []; @@ -783,6 +831,7 @@ async function refreshCrmData() { async function sendPilotText(rawText: string) { const text = rawText.trim(); if (!text || pilotSending.value) return; + const contextPayload = buildContextPayload(); pilotSending.value = true; pilotInput.value = ""; @@ -791,7 +840,16 @@ async function sendPilotText(rawText: string) { pilotLiveLogsExpanded.value = false; pilotLiveLogs.value = []; try { - await pilotChat.sendMessage({ text }); + await pilotChat.sendMessage( + { text }, + contextPayload + ? { + body: { + contextPayload, + }, + } + : undefined, + ); } catch { pilotInput.value = text; } finally { @@ -2918,6 +2976,97 @@ const selectedWorkspaceDealSteps = computed(() => { return [...deal.steps].sort((a, b) => a.order - b.order); }); +function calendarScopeLabel() { + if (focusedCalendarEvent.value) { + return `Календарь: ${focusedCalendarEvent.value.title}`; + } + if (calendarView.value === "month" || calendarView.value === "agenda") { + return `Календарь: ${monthLabel.value}`; + } + if (calendarView.value === "year") { + return `Календарь: ${calendarCursor.value.getFullYear()}`; + } + if (calendarView.value === "week") { + return `Календарь: ${calendarPeriodLabel.value}`; + } + return `Календарь: ${formatDay(`${selectedDateKey.value}T00:00:00`)}`; +} + +function contextScopeLabel(scope: ContextScope) { + if (scope === "summary") return "Summary"; + if (scope === "deal") return "Сделка"; + if (scope === "message") return "Работа с пользователем"; + return calendarScopeLabel(); +} + +const contextScopeChips = computed(() => + contextScopes.value.map((scope) => ({ + scope, + label: contextScopeLabel(scope), + })), +); + +function buildContextPayload(): PilotContextPayload | null { + const scopes = [...contextScopes.value]; + if (!scopes.length) return null; + + const payload: PilotContextPayload = { scopes }; + + if (hasContextScope("summary") && selectedWorkspaceContact.value) { + payload.summary = { + contactId: selectedWorkspaceContact.value.id, + name: selectedWorkspaceContact.value.name, + }; + } + + if (hasContextScope("deal") && selectedWorkspaceDeal.value) { + payload.deal = { + dealId: selectedWorkspaceDeal.value.id, + title: selectedWorkspaceDeal.value.title, + contact: selectedWorkspaceDeal.value.contact, + }; + } + + if (hasContextScope("message")) { + payload.message = { + contactId: selectedWorkspaceContact.value?.id || undefined, + contact: selectedWorkspaceContact.value?.name || selectedCommThread.value?.contact || undefined, + intent: "add_message_or_reminder", + }; + } + + if (hasContextScope("calendar")) { + const eventIds = (() => { + if (focusedCalendarEvent.value) return [focusedCalendarEvent.value.id]; + if (calendarView.value === "day") return selectedDayEvents.value.map((event) => event.id); + if (calendarView.value === "week") return weekDays.value.flatMap((d) => d.events.map((event) => event.id)); + if (calendarView.value === "month" || calendarView.value === "agenda") { + const monthStart = new Date(calendarCursor.value.getFullYear(), calendarCursor.value.getMonth(), 1); + const monthEnd = new Date(calendarCursor.value.getFullYear(), calendarCursor.value.getMonth() + 1, 1); + return sortedEvents.value + .filter((event) => { + const d = new Date(event.start); + return d >= monthStart && d < monthEnd; + }) + .map((event) => event.id); + } + return sortedEvents.value + .filter((event) => new Date(event.start).getFullYear() === calendarCursor.value.getFullYear()) + .map((event) => event.id); + })(); + + payload.calendar = { + view: calendarView.value, + period: calendarPeriodLabel.value, + selectedDateKey: selectedDateKey.value, + focusedEventId: focusedCalendarEvent.value?.id || undefined, + eventIds, + }; + } + + return payload; +} + watch( () => selectedWorkspaceDeal.value?.id ?? "", () => { @@ -3564,6 +3713,17 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
+
+
+ +

{{ pilotMicError }}

@@ -3646,7 +3818,19 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") class="min-h-0 flex-1" :class="selectedTab === 'communications' && peopleLeftMode === 'contacts' ? 'px-0 pt-0 pb-0' : 'px-3 pt-3 pb-0 md:px-4 md:pt-4 md:pb-0'" > -
+
+ {{ contextScopeLabel('calendar') }}
@@ -4553,7 +4737,15 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
-
+
+ Работа с пользователем