pilot: move context pipette to left rail with chip replacement
This commit is contained in:
414
frontend/app.vue
414
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<Record<string, boolean>>({});
|
||||
const eventCloseDraft = ref<Record<string, string>>({});
|
||||
const eventCloseSaving = ref<Record<string, boolean>>({});
|
||||
@@ -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")
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-white/75">{{ pilotHeaderText }}</h2>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-ghost btn-xs btn-square text-white/80 hover:bg-white/10"
|
||||
title="Instructions"
|
||||
@click="openPilotInstructions"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
|
||||
<path d="M19.14 12.94a7.43 7.43 0 0 0 .05-.94 7.43 7.43 0 0 0-.05-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.28 7.28 0 0 0-1.62-.94l-.36-2.54A.5.5 0 0 0 14.9 2h-3.8a.5.5 0 0 0-.49.42l-.36 2.54c-.58.22-1.12.53-1.62.94l-2.39-.96a.5.5 0 0 0-.6.22L3.72 8.84a.5.5 0 0 0 .12.64l2.03 1.58c-.03.31-.05.63-.05.94s.02.63.05.94l-2.03 1.58a.5.5 0 0 0-.12.64l1.92 3.32c.13.23.4.32.64.22l2.39-.96c.5.41 1.04.72 1.62.94l.36 2.54c.04.24.25.42.49.42h3.8c.24 0 .45-.18.49-.42l.36-2.54c.58-.22 1.12-.53 1.62-.94l2.39.96c.24.1.51.01.64-.22l1.92-3.32a.5.5 0 0 0-.12-.64zM13 15.5A3.5 3.5 0 1 1 13 8.5a3.5 3.5 0 0 1 0 7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pilot-threads">
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
@@ -4190,35 +4343,53 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pilot-input-wrap">
|
||||
<div class="pilot-input-shell">
|
||||
<textarea
|
||||
v-model="pilotInput"
|
||||
class="pilot-input-textarea"
|
||||
<div class="pilot-input-wrap">
|
||||
<div class="pilot-input-shell">
|
||||
<textarea
|
||||
v-model="pilotInput"
|
||||
class="pilot-input-textarea"
|
||||
:placeholder="pilotRecording ? 'Recording... speak, then press mic to fill or send to submit' : 'Type a message for Pilot...'"
|
||||
@keydown.enter="handlePilotComposerEnter"
|
||||
/>
|
||||
|
||||
<div v-if="pilotRecording" class="pilot-meter">
|
||||
<div ref="pilotWaveContainer" class="pilot-wave-canvas" />
|
||||
</div>
|
||||
<div v-if="pilotRecording" class="pilot-meter">
|
||||
<div ref="pilotWaveContainer" class="pilot-wave-canvas" />
|
||||
</div>
|
||||
|
||||
<div class="pilot-input-actions">
|
||||
<button
|
||||
class="btn btn-xs btn-circle border border-white/20 bg-transparent text-white/90 hover:bg-white/10"
|
||||
:class="contextPickerEnabled ? 'pilot-mic-active' : ''"
|
||||
:disabled="pilotTranscribing || pilotSending"
|
||||
:title="contextPickerEnabled ? 'Выключить пипетку' : 'Включить пипетку контекста'"
|
||||
@click="toggleContextPicker"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
|
||||
<path d="M19.29 4.71a1 1 0 0 0-1.42 0l-2.58 2.58-1.17-1.17a1 1 0 0 0-1.41 0l-1.42 1.42a1 1 0 0 0 0 1.41l.59.59-5.3 5.3a2 2 0 0 0-.53.92l-.86 3.43a1 1 0 0 0 1.21 1.21l3.43-.86a2 2 0 0 0 .92-.53l5.3-5.3.59.59a1 1 0 0 0 1.41 0l1.42-1.42a1 1 0 0 0 0-1.41l-1.17-1.17 2.58-2.58a1 1 0 0 0 0-1.42z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-circle border border-white/20 bg-transparent text-white/90 hover:bg-white/10"
|
||||
:class="pilotRecording ? 'pilot-mic-active' : ''"
|
||||
:disabled="!pilotMicSupported || pilotTranscribing || pilotSending"
|
||||
<div v-if="!pilotRecording" class="pilot-input-context">
|
||||
<button
|
||||
v-if="contextScopeChips.length === 0"
|
||||
class="context-pipette-trigger"
|
||||
:class="contextPickerEnabled ? 'context-pipette-active' : ''"
|
||||
:disabled="pilotTranscribing || pilotSending"
|
||||
:title="contextPickerEnabled ? 'Выключить пипетку' : 'Включить пипетку контекста'"
|
||||
@click="toggleContextPicker"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
|
||||
<path d="M19.29 4.71a1 1 0 0 0-1.42 0l-2.58 2.58-1.17-1.17a1 1 0 0 0-1.41 0l-1.42 1.42a1 1 0 0 0 0 1.41l.59.59-5.3 5.3a2 2 0 0 0-.53.92l-.86 3.43a1 1 0 0 0 1.21 1.21l3.43-.86a2 2 0 0 0 .92-.53l5.3-5.3.59.59a1 1 0 0 0 1.41 0l1.42-1.42a1 1 0 0 0 0-1.41l-1.17-1.17 2.58-2.58a1 1 0 0 0 0-1.42z" />
|
||||
</svg>
|
||||
<span>Контекст</span>
|
||||
</button>
|
||||
|
||||
<div v-else class="pilot-context-chips">
|
||||
<button
|
||||
v-for="chip in contextScopeChips"
|
||||
:key="`context-chip-${chip.scope}`"
|
||||
type="button"
|
||||
class="context-pipette-chip"
|
||||
@click="removeContextScope(chip.scope)"
|
||||
>
|
||||
{{ chip.label }}
|
||||
<span class="opacity-70">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pilot-input-actions">
|
||||
<button
|
||||
class="btn btn-xs btn-circle border border-white/20 bg-transparent text-white/90 hover:bg-white/10"
|
||||
:class="pilotRecording ? 'pilot-mic-active' : ''"
|
||||
:disabled="!pilotMicSupported || pilotTranscribing || pilotSending"
|
||||
:title="pilotRecording ? 'Stop and insert transcript' : 'Voice input'"
|
||||
@click="togglePilotRecording"
|
||||
>
|
||||
@@ -4237,23 +4408,11 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current" :class="pilotSending ? 'opacity-50' : ''">
|
||||
<path d="M4.5 19.5 21 12 4.5 4.5l.02 5.84L15 12l-10.48 1.66z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="contextScopeChips.length" class="mt-1 flex flex-wrap items-center gap-1.5 px-1">
|
||||
<button
|
||||
v-for="chip in contextScopeChips"
|
||||
:key="`context-chip-${chip.scope}`"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded border border-white/25 bg-white/10 px-2 py-0.5 text-[10px] text-white/90 hover:bg-white/15"
|
||||
@click="removeContextScope(chip.scope)"
|
||||
>
|
||||
{{ chip.label }}
|
||||
<span class="opacity-70">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="pilotMicError" class="pilot-mic-error">{{ pilotMicError }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="pilotMicError" class="pilot-mic-error">{{ pilotMicError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -4261,11 +4420,11 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<div class="workspace-topbar border-b border-base-300 px-3 py-2 md:px-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div v-if="selectedTab === 'communications'" class="join">
|
||||
<div class="join">
|
||||
<button
|
||||
class="btn btn-sm join-item"
|
||||
:class="
|
||||
peopleLeftMode === 'contacts'
|
||||
selectedTab === 'communications' && peopleLeftMode === 'contacts'
|
||||
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
|
||||
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
|
||||
"
|
||||
@@ -4276,7 +4435,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
<button
|
||||
class="btn btn-sm join-item"
|
||||
:class="
|
||||
peopleLeftMode === 'calendar'
|
||||
selectedTab === 'communications' && peopleLeftMode === 'calendar'
|
||||
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
|
||||
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
|
||||
"
|
||||
@@ -4284,8 +4443,18 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
>
|
||||
Calendar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm join-item"
|
||||
:class="
|
||||
selectedTab === 'documents'
|
||||
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
|
||||
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
|
||||
"
|
||||
@click="openDocumentsTab(true)"
|
||||
>
|
||||
Documents
|
||||
</button>
|
||||
</div>
|
||||
<div v-else />
|
||||
|
||||
<div class="dropdown dropdown-end">
|
||||
<button tabindex="0" class="btn btn-sm btn-ghost gap-2">
|
||||
@@ -5233,7 +5402,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
tabindex="0"
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle border border-base-300 bg-base-100 text-base-content/85 hover:bg-base-200"
|
||||
title="Add event"
|
||||
title="Add item"
|
||||
@click.stop="toggleCommQuickMenu"
|
||||
>
|
||||
+
|
||||
@@ -5249,6 +5418,11 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
Log past event
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="openCommDocumentModal">
|
||||
Attach document
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5270,10 +5444,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
@keydown.enter="handleCommComposerEnter"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="commComposerMode !== 'message'"
|
||||
class="comm-event-controls"
|
||||
>
|
||||
<div v-if="commComposerMode === 'planned' || commComposerMode === 'logged'" class="comm-event-controls">
|
||||
<input
|
||||
v-model="commEventForm.startDate"
|
||||
type="date"
|
||||
@@ -5298,6 +5469,25 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
<option :value="90">90m</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-else-if="commComposerMode === 'document'" class="comm-event-controls">
|
||||
<input
|
||||
v-model="commDocumentForm.title"
|
||||
type="text"
|
||||
class="input input-bordered input-xs h-7 min-h-7 flex-1"
|
||||
:disabled="commEventSaving"
|
||||
placeholder="Document title (optional)"
|
||||
>
|
||||
<select
|
||||
v-model="commDocumentForm.type"
|
||||
class="select select-bordered select-xs h-7 min-h-7"
|
||||
:disabled="commEventSaving"
|
||||
>
|
||||
<option value="Regulation">Regulation</option>
|
||||
<option value="Playbook">Playbook</option>
|
||||
<option value="Policy">Policy</option>
|
||||
<option value="Template">Template</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p v-if="commEventError && commComposerMode !== 'message'" class="comm-event-error text-xs text-error">
|
||||
{{ commEventError }}
|
||||
@@ -5352,8 +5542,16 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
<button
|
||||
class="btn btn-sm btn-circle border-0 bg-[#5865f2] text-white hover:bg-[#4752c4]"
|
||||
:disabled="commSending || commEventSaving || !commDraft.trim() || (commComposerMode === 'message' && !commSendChannel)"
|
||||
:title="commComposerMode === 'message' ? `Send via ${commSendChannel}` : (commComposerMode === 'logged' ? 'Save log event' : 'Create event')"
|
||||
@click="commComposerMode === 'message' ? sendCommMessage() : createCommEvent()"
|
||||
:title="
|
||||
commComposerMode === 'message'
|
||||
? `Send via ${commSendChannel}`
|
||||
: commComposerMode === 'logged'
|
||||
? 'Save log event'
|
||||
: commComposerMode === 'document'
|
||||
? 'Save document'
|
||||
: 'Create event'
|
||||
"
|
||||
@click="handleCommComposerSubmit"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current" :class="commSending ? 'opacity-50' : ''">
|
||||
<path d="M4.5 19.5 21 12 4.5 4.5l.02 5.84L15 12l-10.48 1.66z" />
|
||||
@@ -5977,6 +6175,83 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pilot-input-context {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 8px;
|
||||
right: 96px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.context-pipette-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: rgba(245, 247, 255, 0.94);
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
transition: transform 220ms ease, border-color 220ms ease, background-color 220ms ease;
|
||||
}
|
||||
|
||||
.context-pipette-trigger:hover {
|
||||
transform: scale(1.03);
|
||||
border-color: color-mix(in oklab, var(--color-primary) 62%, transparent);
|
||||
background: color-mix(in oklab, var(--color-primary) 18%, transparent);
|
||||
}
|
||||
|
||||
.context-pipette-active {
|
||||
border-color: color-mix(in oklab, var(--color-primary) 72%, transparent);
|
||||
background: color-mix(in oklab, var(--color-primary) 24%, transparent);
|
||||
box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-primary) 38%, transparent) inset;
|
||||
}
|
||||
|
||||
.pilot-context-chips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.pilot-context-chips::-webkit-scrollbar {
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.pilot-context-chips::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.24);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.context-pipette-chip {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(245, 247, 255, 0.95);
|
||||
padding: 4px 9px;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
transition: transform 220ms ease, border-color 220ms ease, background-color 220ms ease;
|
||||
}
|
||||
|
||||
.context-pipette-chip:hover {
|
||||
transform: scale(1.03);
|
||||
border-color: color-mix(in oklab, var(--color-primary) 62%, transparent);
|
||||
background: color-mix(in oklab, var(--color-primary) 20%, transparent);
|
||||
}
|
||||
|
||||
.pilot-meter {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
@@ -6308,16 +6583,19 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
.context-scope-block {
|
||||
position: relative;
|
||||
cursor: crosshair;
|
||||
transition: box-shadow 220ms ease, outline-color 220ms ease, transform 220ms ease;
|
||||
}
|
||||
|
||||
.context-scope-block-active {
|
||||
outline: 2px dashed color-mix(in oklab, var(--color-primary) 55%, transparent);
|
||||
outline: 2px solid color-mix(in oklab, var(--color-primary) 58%, transparent);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-primary) 30%, transparent) inset;
|
||||
}
|
||||
|
||||
.context-scope-block-selected {
|
||||
outline: 2px solid color-mix(in oklab, var(--color-primary) 75%, transparent);
|
||||
outline: 2px solid color-mix(in oklab, var(--color-primary) 72%, transparent);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-primary) 22%, transparent) inset;
|
||||
}
|
||||
|
||||
.context-scope-label {
|
||||
|
||||
Reference in New Issue
Block a user