Seed 20 Odoo+AI prospects and enforce model-only chat responses
This commit is contained in:
170
Frontend/app.vue
170
Frontend/app.vue
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user