From d9c994c408433a44eea3a7cb13e68dc55213c7bf Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Thu, 19 Feb 2026 05:22:16 +0700 Subject: [PATCH] Add chat-side CRM diff panel with keep/rollback flow --- Frontend/app.vue | 151 ++++++- .../graphql/operations/chat-messages.graphql | 10 + .../confirm-latest-change-set.graphql | 6 + .../rollback-latest-change-set.graphql | 6 + Frontend/server/agent/crmAgent.ts | 6 +- Frontend/server/graphql/schema.ts | 122 +++++ Frontend/server/utils/changeSet.ts | 415 ++++++++++++++++++ 7 files changed, 709 insertions(+), 7 deletions(-) create mode 100644 Frontend/graphql/operations/confirm-latest-change-set.graphql create mode 100644 Frontend/graphql/operations/rollback-latest-change-set.graphql create mode 100644 Frontend/server/utils/changeSet.ts diff --git a/Frontend/app.vue b/Frontend/app.vue index 7ef1053..88b9fa7 100644 --- a/Frontend/app.vue +++ b/Frontend/app.vue @@ -13,6 +13,8 @@ import updateFeedDecisionMutation from "./graphql/operations/update-feed-decisio import chatConversationsQuery from "./graphql/operations/chat-conversations.graphql?raw"; import createChatConversationMutation from "./graphql/operations/create-chat-conversation.graphql?raw"; import selectChatConversationMutation from "./graphql/operations/select-chat-conversation.graphql?raw"; +import confirmLatestChangeSetMutation from "./graphql/operations/confirm-latest-change-set.graphql?raw"; +import rollbackLatestChangeSetMutation from "./graphql/operations/rollback-latest-change-set.graphql?raw"; type TabId = "communications" | "documents"; type CalendarView = "day" | "week" | "month" | "year" | "agenda"; type SortMode = "name" | "lastContact"; @@ -190,6 +192,16 @@ type PilotMessage = { output: string; at: string; }> | null; + changeSetId?: string | null; + changeStatus?: "pending" | "confirmed" | "rolled_back" | null; + changeSummary?: string | null; + changeItems?: Array<{ + entity: string; + action: string; + title: string; + before: string; + after: string; + }> | null; createdAt?: string; }; @@ -388,7 +400,7 @@ async function sendPilotMessage() { }, 450); try { await gqlFetch<{ sendPilotMessage: { ok: boolean } }>(sendPilotMessageMutation, { text }); - await Promise.all([loadPilotMessages(), loadChatConversations()]); + await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData()]); } catch { pilotInput.value = text; } finally { @@ -397,6 +409,39 @@ async function sendPilotMessage() { } } +const changePanelOpen = ref(true); +const changeActionBusy = ref(false); + +const latestChangeMessage = computed(() => { + return ( + [...pilotMessages.value] + .reverse() + .find((m) => m.role === "assistant" && m.changeSetId && m.changeStatus !== "rolled_back") ?? null + ); +}); + +async function confirmLatestChangeSet() { + if (changeActionBusy.value || !latestChangeMessage.value?.changeSetId) return; + changeActionBusy.value = true; + try { + await gqlFetch<{ confirmLatestChangeSet: { ok: boolean } }>(confirmLatestChangeSetMutation); + await loadPilotMessages(); + } finally { + changeActionBusy.value = false; + } +} + +async function rollbackLatestChangeSet() { + if (changeActionBusy.value || !latestChangeMessage.value?.changeSetId) return; + changeActionBusy.value = true; + try { + await gqlFetch<{ rollbackLatestChangeSet: { ok: boolean } }>(rollbackLatestChangeSetMutation); + await Promise.all([loadPilotMessages(), refreshCrmData()]); + } finally { + changeActionBusy.value = false; + } +} + onMounted(() => { loadMe() .then(() => Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData()])) @@ -820,10 +865,13 @@ const selectedCommThread = computed(() => const selectedCommChannel = ref<"All" | CommItem["channel"]>("All"); const rightSidebarMode = ref<"summary" | "pinned">("summary"); +const commDraft = ref(""); +const commSending = ref(false); watch(selectedCommThreadId, () => { selectedCommChannel.value = "All"; rightSidebarMode.value = "summary"; + commDraft.value = ""; }); const commChannelTabs = computed(() => { @@ -1064,6 +1112,35 @@ function openMessageFromContact(channel: CommItem["channel"]) { selectedCommChannel.value = channel; } +async function sendCommMessage() { + const text = commDraft.value.trim(); + if (!text || commSending.value || !selectedCommThread.value) return; + + commSending.value = true; + try { + const fallback = + selectedCommThread.value.channels.find((channel) => channel !== "Phone") ?? + "Telegram"; + const channel = selectedCommChannel.value === "All" ? fallback : selectedCommChannel.value; + + await gqlFetch<{ createCommunication: { ok: boolean; id: string } }>(createCommunicationMutation, { + input: { + contact: selectedCommThread.value.contact, + channel, + kind: "message", + direction: "out", + text, + }, + }); + + commDraft.value = ""; + await refreshCrmData(); + openCommunicationThread(selectedCommThread.value.contact); + } finally { + commSending.value = false; + } +} + async function executeFeedAction(card: FeedCard) { const key = card.proposal.key; if (key === "create_followup") { @@ -1328,6 +1405,52 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") +
+
+
+

Detected changes

+

+ {{ latestChangeMessage.changeSummary || `Changed: ${latestChangeMessage.changeItems.length}` }} + · status: {{ latestChangeMessage.changeStatus || "pending" }} +

+
+
+ + + +
+
+
+
+

{{ item.title }}

+

{{ item.entity }} · {{ item.action }}

+

- {{ item.before }}

+

+ {{ item.after }}

+
+
+
+
Calendar
-
-