Seed 20 Odoo+AI prospects and enforce model-only chat responses

This commit is contained in:
Ruslan Bakiev
2026-02-18 21:50:41 +07:00
parent fdc85d5c42
commit 693faa8621
3 changed files with 293 additions and 157 deletions

View File

@@ -100,6 +100,7 @@ const tabs: { id: TabId; label: string }[] = [
];
const selectedTab = ref<TabId>("communications");
const pilotSettingsOpen = ref(false);
function dayKey(date: Date) {
const y = date.getFullYear();
@@ -221,6 +222,19 @@ const loginPassword = ref("");
const loginError = ref<string | null>(null);
const loginBusy = ref(false);
const userMood = computed(() => {
const hour = new Date().getHours();
if (hour < 12) return "⚡";
if (hour < 18) return "🚀";
return "🌙";
});
const userInitial = computed(() => {
const name = authMe.value?.user?.name?.trim();
if (!name) return "U";
return name.charAt(0).toUpperCase();
});
const activeChatConversation = computed<ChatConversation | null>(() => {
const activeId = authMe.value?.conversation.id;
if (!activeId) return null;
@@ -734,6 +748,13 @@ watchEffect(() => {
const selectedDocument = computed(() => documents.value.find((item) => item.id === selectedDocumentId.value));
function openPilotDocument(docId: string) {
if (!docId) return;
selectedTab.value = "documents";
selectedDocumentId.value = docId;
pilotSettingsOpen.value = false;
}
const peopleLeftMode = ref<PeopleLeftMode>("contacts");
const peopleListMode = ref<"contacts" | "deals">("contacts");
const peopleSearch = ref("");
@@ -1049,6 +1070,12 @@ function openDealThread(deal: Deal) {
}
function openThreadFromCalendarItem(event: CalendarEvent) {
if (!event.contact?.trim()) {
selectedTab.value = "communications";
peopleLeftMode.value = "calendar";
pickDate(event.start.slice(0, 10));
return;
}
openCommunicationThread(event.contact);
selectedCommChannel.value = "All";
}
@@ -1179,8 +1206,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</script>
<template>
<div class="min-h-screen p-3 md:p-5">
<div v-if="!authMe" class="flex min-h-[calc(100vh-2.5rem)] items-center justify-center">
<div class="h-[100dvh] overflow-hidden bg-base-200/35">
<div v-if="!authMe" class="flex h-full items-center justify-center px-3">
<div class="card w-full max-w-sm border border-base-300 bg-base-100 shadow-sm">
<div class="card-body p-5">
<h1 class="text-lg font-semibold">Login</h1>
@@ -1210,9 +1237,9 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
<template v-else>
<div class="grid gap-3 lg:grid-cols-12 lg:gap-4">
<aside class="pilot-shell card min-h-0 border border-base-300 shadow-sm lg:col-span-3 lg:sticky lg:top-5 lg:h-[calc(100vh-2.5rem)]">
<div class="card-body h-full min-h-0 p-0">
<div class="grid h-full min-h-0 grid-cols-1 gap-0 lg:grid-cols-[320px_minmax(0,1fr)]">
<aside class="pilot-shell min-h-0 border-r border-base-300">
<div class="flex h-full min-h-0 flex-col p-0">
<div class="pilot-header">
<div>
<h2 class="text-sm font-semibold uppercase tracking-wide text-white/75">Pilot Chat</h2>
@@ -1220,7 +1247,31 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
{{ authMe.team.name }} · {{ authMe.user.name }} · {{ authMe.conversation.title }}
</p>
</div>
<button class="btn btn-ghost btn-xs text-white/80 hover:bg-white/10" @click="logout">Logout</button>
<button
class="btn btn-ghost btn-xs btn-square text-white/80 hover:bg-white/10"
title="Pilot settings"
@click="pilotSettingsOpen = !pilotSettingsOpen"
>
<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 v-if="pilotSettingsOpen" class="pilot-settings-panel">
<p class="text-[11px] font-semibold uppercase tracking-wide text-white/65">Pilot Settings</p>
<p class="mt-1 text-xs text-white/70">Prompt sources from workspace documents.</p>
<div class="mt-2 max-h-44 space-y-1 overflow-y-auto pr-1">
<button
v-for="doc in documents.slice(0, 10)"
:key="`pilot-doc-${doc.id}`"
class="w-full rounded-lg border border-white/10 bg-white/5 px-2 py-1.5 text-left text-xs text-white/85 transition hover:bg-white/10"
@click="openPilotDocument(doc.id)"
>
<p class="truncate font-medium">{{ doc.title }}</p>
<p class="truncate text-[11px] text-white/55">{{ doc.type }} · {{ doc.owner }}</p>
</button>
<p v-if="documents.length === 0" class="text-xs text-white/55">No documents yet.</p>
</div>
</div>
<div class="pilot-threads">
@@ -1327,10 +1378,22 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</aside>
<main class="card border border-base-300 bg-base-100 shadow-sm lg:col-span-9">
<div class="card-body flex flex-col p-3 pb-20 md:p-4 md:pb-24">
<div class="flex-1">
<section v-if="selectedTab === 'communications' && peopleLeftMode === 'calendar'" class="space-y-3">
<main class="min-h-0 bg-base-100">
<div class="flex h-full min-h-0 flex-col pb-20 md:pb-24">
<div class="workspace-topbar border-b border-base-300 px-3 py-2 md:px-4">
<div class="ml-auto flex items-center gap-2">
<div class="inline-flex items-center gap-2 rounded-xl border border-base-300 bg-base-100 px-2 py-1.5">
<div class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-primary/15 text-sm font-semibold text-primary">{{ userInitial }}</div>
<div class="min-w-0">
<p class="truncate text-xs font-semibold">{{ authMe.user.name }} {{ userMood }}</p>
<p class="truncate text-[11px] text-base-content/60">{{ authMe.user.phone }}</p>
</div>
</div>
<button class="btn btn-sm btn-outline" @click="logout">Logout</button>
</div>
</div>
<div class="min-h-0 flex-1 p-3 md:p-4">
<section v-if="selectedTab === 'communications' && peopleLeftMode === 'calendar'" class="flex h-full min-h-0 flex-col gap-3">
<div class="mb-1 flex justify-end">
<div class="join">
<button class="btn btn-sm join-item btn-ghost" @click="peopleLeftMode = 'contacts'">Contacts</button>
@@ -1361,6 +1424,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</div>
<div class="min-h-0 flex-1 overflow-y-auto pr-1">
<div v-if="calendarView === 'month'" class="space-y-1">
<div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-base-content/60">
<span>Sun</span>
@@ -1384,9 +1448,14 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
@click="pickDate(cell.key)"
>
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
<p v-for="event in cell.events.slice(0, 2)" :key="event.id" class="truncate text-[10px] text-base-content/70">
<button
v-for="event in cell.events.slice(0, 2)"
:key="event.id"
class="block w-full truncate text-left text-[10px] text-base-content/70 hover:underline"
@click.stop="openThreadFromCalendarItem(event)"
>
{{ formatTime(event.start) }} {{ event.title }}
</p>
</button>
</button>
</div>
</div>
@@ -1401,21 +1470,31 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
>
<p class="mb-2 text-sm font-semibold">{{ day.label }} {{ day.day }}</p>
<div class="space-y-1">
<p v-for="event in day.events" :key="event.id" class="rounded bg-base-200 px-2 py-1 text-xs">
<button
v-for="event in day.events"
:key="event.id"
class="block w-full rounded bg-base-200 px-2 py-1 text-left text-xs hover:bg-base-300/80"
@click.stop="openThreadFromCalendarItem(event)"
>
{{ formatTime(event.start) }} - {{ event.title }} ({{ event.contact }})
</p>
</button>
<p v-if="day.events.length === 0" class="text-xs text-base-content/50">No events</p>
</div>
</article>
</div>
<div v-else-if="calendarView === 'day'" class="space-y-2">
<article v-for="event in selectedDayEvents" :key="event.id" class="rounded-xl border border-base-300 p-3">
<button
v-for="event in selectedDayEvents"
:key="event.id"
class="block w-full rounded-xl border border-base-300 p-3 text-left transition hover:bg-base-200/60"
@click="openThreadFromCalendarItem(event)"
>
<p class="font-medium">{{ event.title }}</p>
<p class="text-xs text-base-content/60">{{ event.contact }}</p>
<p class="text-xs text-base-content/60">{{ formatTime(event.start) }} - {{ formatTime(event.end) }}</p>
<p class="mt-1 text-sm text-base-content/80">{{ event.note }}</p>
</article>
</button>
<p v-if="selectedDayEvents.length === 0" class="text-sm text-base-content/60">No events on this day.</p>
</div>
@@ -1428,19 +1507,29 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
>
<p class="font-medium">{{ item.label }}</p>
<p class="text-xs text-base-content/60">{{ item.count }} events</p>
<p v-if="item.first" class="mt-1 text-xs text-base-content/70">
<button
v-if="item.first"
class="mt-1 block w-full text-left text-xs text-base-content/70 hover:underline"
@click.stop="openThreadFromCalendarItem(item.first)"
>
{{ formatDay(item.first.start) }} · {{ item.first.title }}
</p>
</button>
</button>
</div>
<div v-else class="space-y-2">
<article v-for="event in sortedEvents" :key="event.id" class="rounded-xl border border-base-300 p-3">
<button
v-for="event in sortedEvents"
:key="event.id"
class="block w-full rounded-xl border border-base-300 p-3 text-left transition hover:bg-base-200/60"
@click="openThreadFromCalendarItem(event)"
>
<p class="font-medium">{{ event.title }}</p>
<p class="text-xs text-base-content/60">{{ event.contact }}</p>
<p class="text-xs text-base-content/60">{{ formatDay(event.start) }} · {{ formatTime(event.start) }} - {{ formatTime(event.end) }}</p>
<p class="mt-1 text-sm text-base-content/80">{{ event.note }}</p>
</article>
</button>
</div>
</div>
</section>
@@ -1623,15 +1712,15 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</section>
<section v-else-if="selectedTab === 'communications' && peopleLeftMode === 'contacts'" class="space-y-3">
<section v-else-if="selectedTab === 'communications' && peopleLeftMode === 'contacts'" class="flex h-full min-h-0 flex-col gap-3">
<div class="mb-1 flex justify-end">
<div class="join">
<button class="btn btn-sm join-item btn-primary" @click="peopleLeftMode = 'contacts'">Contacts</button>
<button class="btn btn-sm join-item btn-ghost" @click="peopleLeftMode = 'calendar'">Calendar</button>
</div>
</div>
<div class="grid gap-3 md:grid-cols-[220px_minmax(0,1fr)_320px]">
<aside class="min-h-0 rounded-xl border border-base-300">
<div class="grid min-h-0 flex-1 gap-0 md:grid-cols-[220px_minmax(0,1fr)_320px]">
<aside class="min-h-0 border-r border-base-300 flex flex-col">
<div class="sticky top-0 z-20 border-b border-base-300 bg-base-100 p-2">
<div class="mb-2 flex items-center gap-1">
<button
@@ -1687,7 +1776,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</div>
<div class="min-h-0 space-y-1.5 overflow-y-auto p-2">
<div class="min-h-0 flex-1 space-y-1.5 overflow-y-auto p-2">
<button
v-if="peopleListMode === 'contacts'"
v-for="thread in peopleContactList"
@@ -1752,7 +1841,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</aside>
<article class="min-h-0 rounded-xl border border-base-300">
<article class="min-h-0 border-r border-base-300 flex flex-col">
<div v-if="false" class="p-3">
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-2">
<div class="flex items-center gap-1">
@@ -1881,7 +1970,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</div>
<div v-else-if="selectedCommThread" class="relative flex flex-col p-3">
<div v-else-if="selectedCommThread" class="relative flex h-full min-h-0 flex-col p-3">
<div class="mb-3 flex items-start justify-between gap-2 border-b border-base-300 pb-2">
<div>
<p class="font-medium">{{ selectedCommThread.contact }}</p>
@@ -1933,7 +2022,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</button>
</div>
<div class="space-y-2 pr-1">
<div class="min-h-0 flex-1 space-y-2 overflow-y-auto pr-1">
<div v-for="entry in threadStreamItems" :key="entry.id">
<div v-if="entry.kind === 'call'" class="flex justify-center">
<div class="call-wave-card w-full max-w-[460px] rounded-2xl border border-base-300 px-4 py-3 text-center">
@@ -2036,8 +2125,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</article>
<aside class="min-h-0 rounded-xl border border-base-300">
<div v-if="selectedWorkspaceContact" class="h-full p-3">
<aside class="min-h-0">
<div v-if="selectedWorkspaceContact" class="flex h-full min-h-0 flex-col p-3">
<div class="mb-3 flex items-start justify-between gap-2 border-b border-base-300 pb-2">
<div>
<p class="font-medium">{{ selectedWorkspaceContact.name }}</p>
@@ -2058,6 +2147,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</button>
</div>
<div class="min-h-0 flex-1 overflow-y-auto pr-1">
<div v-if="rightSidebarMode === 'pinned'" class="space-y-2">
<article
v-for="item in selectedCommPinnedStream"
@@ -2106,6 +2196,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
/>
</div>
</template>
</div>
</div>
<div v-else class="flex h-full items-center justify-center text-sm text-base-content/60">
@@ -2115,7 +2206,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</section>
<section v-else-if="selectedTab === 'documents'" class="space-y-3">
<section v-else-if="selectedTab === 'documents'" class="flex h-full min-h-0 flex-col gap-3">
<div class="rounded-xl border border-base-300 p-3">
<div class="grid gap-2 md:grid-cols-[1fr_220px]">
<label class="form-control">
@@ -2137,9 +2228,9 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</div>
<div class="grid gap-3 md:grid-cols-12">
<aside class="min-h-0 rounded-xl border border-base-300 md:col-span-4">
<div class="min-h-0 space-y-2 overflow-y-auto p-2">
<div class="grid min-h-0 flex-1 gap-0 md:grid-cols-12">
<aside class="min-h-0 border-r border-base-300 md:col-span-4 flex flex-col">
<div class="min-h-0 flex-1 space-y-2 overflow-y-auto p-2">
<button
v-for="doc in filteredDocuments"
:key="doc.id"
@@ -2155,8 +2246,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</aside>
<article class="min-h-0 rounded-xl border border-base-300 md:col-span-8">
<div v-if="selectedDocument" class="p-3 md:p-4">
<article class="min-h-0 md:col-span-8 flex flex-col">
<div v-if="selectedDocument" class="flex h-full min-h-0 flex-col p-3 md:p-4">
<div class="border-b border-base-300 pb-2">
<p class="font-medium">{{ selectedDocument.title }}</p>
<p class="text-xs text-base-content/60">
@@ -2165,7 +2256,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<p class="mt-1 text-sm text-base-content/80">{{ selectedDocument.summary }}</p>
</div>
<div class="mt-3">
<div class="mt-3 min-h-0 flex-1 overflow-y-auto">
<ContactCollaborativeEditor
:key="`doc-editor-${selectedDocument.id}`"
v-model="selectedDocument.body"
@@ -2226,12 +2317,19 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
.pilot-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
padding: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(8, 10, 16, 0.2);
}
.pilot-settings-panel {
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(8, 10, 16, 0.34);
padding: 10px;
}
.pilot-threads {
padding: 10px 10px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);