From 38fcb1bfcce5b3244243802779d492e97452746f Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Mon, 23 Feb 2026 07:45:28 +0700 Subject: [PATCH] pilot: move context pipette to left rail with chip replacement --- frontend/app.vue | 414 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 346 insertions(+), 68 deletions(-) diff --git a/frontend/app.vue b/frontend/app.vue index 180919e..fb71d60 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -9,6 +9,7 @@ import logPilotNoteMutation from "./graphql/operations/log-pilot-note.graphql?ra import createCalendarEventMutation from "./graphql/operations/create-calendar-event.graphql?raw"; import archiveCalendarEventMutation from "./graphql/operations/archive-calendar-event.graphql?raw"; import createCommunicationMutation from "./graphql/operations/create-communication.graphql?raw"; +import createWorkspaceDocumentMutation from "./graphql/operations/create-workspace-document.graphql?raw"; import updateCommunicationTranscriptMutation from "./graphql/operations/update-communication-transcript.graphql?raw"; import updateFeedDecisionMutation from "./graphql/operations/update-feed-decision.graphql?raw"; import chatConversationsQuery from "./graphql/operations/chat-conversations.graphql?raw"; @@ -1623,8 +1624,12 @@ function normalizedConversationId() { } function currentUiPath() { - if (selectedTab.value !== "communications") { - return withReviewQuery(`/chat/${encodeURIComponent(normalizedConversationId())}`); + if (selectedTab.value === "documents") { + const docId = selectedDocumentId.value.trim(); + if (docId) { + return withReviewQuery(`/documents/${encodeURIComponent(docId)}`); + } + return withReviewQuery("/documents"); } if (peopleLeftMode.value === "calendar") { @@ -1756,6 +1761,15 @@ function applyPathToUi(pathname: string, search = "") { return; } + const documentsMatch = path.match(/^\/documents(?:\/([^/]+))?\/?$/i); + if (documentsMatch) { + const rawDocumentId = decodeURIComponent(documentsMatch[1] ?? "").trim(); + selectedTab.value = "documents"; + focusedCalendarEventId.value = ""; + if (rawDocumentId) selectedDocumentId.value = rawDocumentId; + return; + } + const contactMatch = path.match(/^\/contact\/([^/]+)\/?$/i); if (contactMatch) { const rawContactId = decodeURIComponent(contactMatch[1] ?? "").trim(); @@ -2789,6 +2803,40 @@ const selectedContactRecentMessages = computed(() => { .slice(0, 8); }); +const CONTACT_DOCUMENT_SCOPE_PREFIX = "contact:"; + +function buildContactDocumentScope(contactId: string, contactName: string) { + return `${CONTACT_DOCUMENT_SCOPE_PREFIX}${encodeURIComponent(contactId)}:${encodeURIComponent(contactName)}`; +} + +function parseContactDocumentScope(scope: string) { + const raw = String(scope ?? "").trim(); + if (!raw.startsWith(CONTACT_DOCUMENT_SCOPE_PREFIX)) return null; + const payload = raw.slice(CONTACT_DOCUMENT_SCOPE_PREFIX.length); + const [idRaw, ...nameParts] = payload.split(":"); + const contactId = decodeURIComponent(idRaw ?? "").trim(); + const contactName = decodeURIComponent(nameParts.join(":") ?? "").trim(); + if (!contactId) return null; + return { + contactId, + contactName, + }; +} + +function formatDocumentScope(scope: string) { + const linked = parseContactDocumentScope(scope); + if (!linked) return scope; + return linked.contactName ? `Contact · ${linked.contactName}` : "Contact document"; +} + +function isDocumentLinkedToContact(doc: WorkspaceDocument, contact: Contact | null | undefined) { + if (!contact) return false; + const linked = parseContactDocumentScope(doc.scope); + if (!linked) return false; + if (linked.contactId) return linked.contactId === contact.id; + return Boolean(linked.contactName && linked.contactName === contact.name); +} + const documentSearch = ref(""); const selectedDocumentType = ref<"All" | WorkspaceDocument["type"]>("All"); @@ -2803,7 +2851,7 @@ const filteredDocuments = computed(() => { .filter((item) => { if (selectedDocumentType.value !== "All" && item.type !== selectedDocumentType.value) return false; if (!query) return true; - const haystack = [item.title, item.summary, item.owner, item.scope, item.body].join(" ").toLowerCase(); + const haystack = [item.title, item.summary, item.owner, formatDocumentScope(item.scope), item.body].join(" ").toLowerCase(); return haystack.includes(query); }) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); @@ -2825,12 +2873,14 @@ watchEffect(() => { const selectedDocument = computed(() => documents.value.find((item) => item.id === selectedDocumentId.value)); -function openPilotInstructions() { +function openDocumentsTab(push = false) { selectedTab.value = "documents"; + focusedCalendarEventId.value = ""; if (!selectedDocumentId.value && filteredDocuments.value.length) { const first = filteredDocuments.value[0]; if (first) selectedDocumentId.value = first.id; } + syncPathFromUi(push); } const peopleListMode = ref<"contacts" | "deals">("contacts"); @@ -2936,6 +2986,7 @@ watch( focusedCalendarEventId.value, selectedContactId.value, selectedDealId.value, + selectedDocumentId.value, activeChangeSetId.value, activeChangeStep.value, ], @@ -2950,7 +3001,7 @@ const commPinnedOnly = ref(false); const commDraft = ref(""); const commSending = ref(false); const commRecording = ref(false); -const commComposerMode = ref<"message" | "planned" | "logged">("message"); +const commComposerMode = ref<"message" | "planned" | "logged" | "document">("message"); const commQuickMenuOpen = ref(false); const commEventSaving = ref(false); const commEventError = ref(""); @@ -2960,6 +3011,13 @@ const commEventForm = ref({ startTime: "", durationMinutes: 30, }); +const commDocumentForm = ref<{ + type: WorkspaceDocument["type"]; + title: string; +}>({ + type: "Template", + title: "", +}); const eventCloseOpen = ref>({}); const eventCloseDraft = ref>({}); const eventCloseSaving = ref>({}); @@ -2985,6 +3043,7 @@ watch(selectedCommThreadId, () => { commComposerMode.value = "message"; commQuickMenuOpen.value = false; commEventError.value = ""; + commDocumentForm.value = { type: "Template", title: "" }; eventCloseOpen.value = {}; eventCloseDraft.value = {}; eventCloseSaving.value = {}; @@ -3321,6 +3380,34 @@ const selectedWorkspaceContact = computed(() => { return contacts.value[0] ?? null; }); +const contactRightPanelMode = ref<"summary" | "documents">("summary"); +const contactDocumentsSearch = ref(""); + +const selectedWorkspaceContactDocuments = computed(() => { + const contact = selectedWorkspaceContact.value; + if (!contact) return []; + return documents.value + .filter((doc) => isDocumentLinkedToContact(doc, contact)) + .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); +}); + +const filteredSelectedWorkspaceContactDocuments = computed(() => { + const query = contactDocumentsSearch.value.trim().toLowerCase(); + if (!query) return selectedWorkspaceContactDocuments.value; + return selectedWorkspaceContactDocuments.value.filter((doc) => { + const haystack = [doc.title, doc.summary, doc.owner, formatDocumentScope(doc.scope), doc.body].join(" ").toLowerCase(); + return haystack.includes(query); + }); +}); + +watch( + () => selectedWorkspaceContact.value?.id ?? "", + () => { + contactRightPanelMode.value = "summary"; + contactDocumentsSearch.value = ""; + }, +); + const selectedWorkspaceDeal = computed(() => { const explicit = deals.value.find((deal) => deal.id === selectedDealId.value); if (explicit) return explicit; @@ -3723,6 +3810,13 @@ function setDefaultCommEventForm(mode: "planned" | "logged") { }; } +function setDefaultCommDocumentForm() { + commDocumentForm.value = { + type: "Template", + title: "", + }; +} + function openCommEventModal(mode: "planned" | "logged") { if (!selectedCommThread.value) return; commEventMode.value = mode; @@ -3732,10 +3826,19 @@ function openCommEventModal(mode: "planned" | "logged") { commQuickMenuOpen.value = false; } +function openCommDocumentModal() { + if (!selectedCommThread.value) return; + setDefaultCommDocumentForm(); + commEventError.value = ""; + commComposerMode.value = "document"; + commQuickMenuOpen.value = false; +} + function closeCommEventModal() { if (commEventSaving.value) return; commComposerMode.value = "message"; commEventError.value = ""; + setDefaultCommDocumentForm(); commQuickMenuOpen.value = false; } @@ -3751,6 +3854,7 @@ function closeCommQuickMenu() { function commComposerPlaceholder() { if (commComposerMode.value === "planned") return "Опиши, что нужно запланировать..."; if (commComposerMode.value === "logged") return "Опиши итог/отчёт по прошедшему событию..."; + if (commComposerMode.value === "document") return "Опиши документ или вложение для контакта..."; return "Type a message..."; } @@ -3763,6 +3867,15 @@ function buildCommEventTitle(text: string, mode: "planned" | "logged", contact: return mode === "logged" ? `Отчёт по контакту ${contact}` : `Событие с ${contact}`; } +function buildCommDocumentTitle(text: string, contact: string) { + const cleaned = text.replace(/\s+/g, " ").trim(); + if (cleaned) { + const sentence = cleaned.split(/[.!?\n]/)[0]?.trim() ?? ""; + if (sentence) return sentence.slice(0, 120); + } + return `Документ для ${contact}`; +} + async function createCommEvent() { if (!selectedCommThread.value || commEventSaving.value) return; @@ -3816,6 +3929,47 @@ async function createCommEvent() { } } +async function createCommDocument() { + if (!selectedCommThread.value || commEventSaving.value) return; + + const summary = commDraft.value.trim(); + if (!summary) { + commEventError.value = "Текст документа обязателен"; + return; + } + + const title = commDocumentForm.value.title.trim() || buildCommDocumentTitle(summary, selectedCommThread.value.contact); + const scope = buildContactDocumentScope(selectedCommThread.value.id, selectedCommThread.value.contact); + const body = summary; + + commEventSaving.value = true; + commEventError.value = ""; + try { + const res = await gqlFetch<{ createWorkspaceDocument: WorkspaceDocument }>(createWorkspaceDocumentMutation, { + input: { + title, + type: commDocumentForm.value.type, + owner: authDisplayName.value, + scope, + summary, + body, + }, + }); + + documents.value = [res.createWorkspaceDocument, ...documents.value.filter((doc) => doc.id !== res.createWorkspaceDocument.id)]; + selectedDocumentId.value = res.createWorkspaceDocument.id; + contactRightPanelMode.value = "documents"; + commDraft.value = ""; + commComposerMode.value = "message"; + commEventError.value = ""; + setDefaultCommDocumentForm(); + } catch (error: any) { + commEventError.value = String(error?.message ?? error ?? "Failed to create document"); + } finally { + commEventSaving.value = false; + } +} + async function sendCommMessage() { const text = commDraft.value.trim(); if (!text || commSending.value || !selectedCommThread.value) return; @@ -3850,10 +4004,18 @@ function toggleCommRecording() { function handleCommComposerEnter(event: KeyboardEvent) { if (event.shiftKey) return; event.preventDefault(); + handleCommComposerSubmit(); +} + +function handleCommComposerSubmit() { if (commComposerMode.value === "message") { void sendCommMessage(); return; } + if (commComposerMode.value === "document") { + void createCommDocument(); + return; + } void createCommEvent(); } @@ -4011,15 +4173,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")

{{ pilotHeaderText }}

-
@@ -4190,35 +4343,53 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
-
-
-