DB-backed workspace + LangGraph agent
This commit is contained in:
640
Frontend/app.vue
640
Frontend/app.vue
@@ -148,351 +148,19 @@ function endAfter(startIso: string, minutes: number) {
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
const feedCards = ref<FeedCard[]>([
|
||||
{
|
||||
id: "f1",
|
||||
at: atOffset(0, 9, 35),
|
||||
contact: "Anna Meyer",
|
||||
text: "I analyzed Anna Meyer's latest activity: after a demo, the decision window is usually open for 1-2 hours. I suggest scheduling a follow-up immediately to keep momentum.",
|
||||
proposal: {
|
||||
title: "Add event to calendar",
|
||||
details: [
|
||||
"Contact: Anna Meyer",
|
||||
"Start: 30 minutes from now",
|
||||
"Duration: 30 minutes",
|
||||
],
|
||||
key: "create_followup",
|
||||
},
|
||||
decision: "pending",
|
||||
},
|
||||
{
|
||||
id: "f2",
|
||||
at: atOffset(0, 10, 8),
|
||||
contact: "Murat Ali",
|
||||
text: "I found that Murat Ali gave 3 quick replies in a row over the last hour. I suggest moving to a short call now while engagement is high.",
|
||||
proposal: {
|
||||
title: "Start a call and open chat",
|
||||
details: [
|
||||
"Contact: Murat Ali",
|
||||
"Channel: Phone",
|
||||
"After action: open the communication thread for this contact",
|
||||
],
|
||||
key: "call",
|
||||
},
|
||||
decision: "pending",
|
||||
},
|
||||
]);
|
||||
const feedCards = ref<FeedCard[]>([]);
|
||||
|
||||
const contacts = ref<Contact[]>([
|
||||
{
|
||||
id: "c1",
|
||||
name: "Anna Meyer",
|
||||
avatar: "https://randomuser.me/api/portraits/women/44.jpg",
|
||||
company: "Nordline GmbH",
|
||||
country: "Germany",
|
||||
location: "Berlin",
|
||||
channels: ["Telegram", "Phone", "Email"],
|
||||
lastContactAt: atOffset(-0, 10, 20),
|
||||
description: "Decision owner for procurement. Prefers short, concrete updates with clear deadlines.\n\nBest pattern: one clear question per message, then one explicit next step.\n\nRisk: if timeline question is delayed, decision date keeps slipping.",
|
||||
},
|
||||
{
|
||||
id: "c2",
|
||||
name: "Murat Ali",
|
||||
avatar: "https://randomuser.me/api/portraits/men/32.jpg",
|
||||
company: "Connect FZCO",
|
||||
country: "UAE",
|
||||
location: "Dubai",
|
||||
channels: ["WhatsApp", "Instagram", "Phone", "Email"],
|
||||
lastContactAt: atOffset(-1, 18, 10),
|
||||
description: "High activity in chat, fast response cycle.\n\nNeeds legal path clarity and explicit owner from his side.\n\nBest move: in next call, lock legal owner and target signature date.",
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
name: "Ilya Petroff",
|
||||
avatar: "https://randomuser.me/api/portraits/men/18.jpg",
|
||||
company: "Volta Tech",
|
||||
country: "Armenia",
|
||||
location: "Yerevan",
|
||||
channels: ["Email", "Phone"],
|
||||
lastContactAt: atOffset(-1, 11, 12),
|
||||
description: "Early-stage lead. Interested, but wants structured onboarding before commercial details.\n\nBest move: send concise onboarding plan + 2 time slots for intro session.",
|
||||
},
|
||||
{
|
||||
id: "c4",
|
||||
name: "Carlos Rivera",
|
||||
avatar: "https://randomuser.me/api/portraits/men/65.jpg",
|
||||
company: "BluePort",
|
||||
country: "Spain",
|
||||
location: "Barcelona",
|
||||
channels: ["WhatsApp", "Email"],
|
||||
lastContactAt: atOffset(-3, 14, 35),
|
||||
description: "Pilot interest for two teams. Focus on rollout timeline and ownership per team.\n\nBest move: propose phased pilot with weekly checkpoints.",
|
||||
},
|
||||
{
|
||||
id: "c5",
|
||||
name: "Daria Ivanova",
|
||||
avatar: "https://randomuser.me/api/portraits/women/22.jpg",
|
||||
company: "Skyline Trade",
|
||||
country: "Kazakhstan",
|
||||
location: "Almaty",
|
||||
channels: ["Telegram", "Phone"],
|
||||
lastContactAt: atOffset(-4, 16, 10),
|
||||
description: "Commercial discussion is blocked by ROI framing.\n\nBest move: provide short ROI model with three measurable outcomes.",
|
||||
},
|
||||
{
|
||||
id: "c6",
|
||||
name: "Ethan Moore",
|
||||
avatar: "https://randomuser.me/api/portraits/men/76.jpg",
|
||||
company: "NorthBridge",
|
||||
country: "USA",
|
||||
location: "Austin",
|
||||
channels: ["Email", "Phone"],
|
||||
lastContactAt: atOffset(-5, 12, 45),
|
||||
description: "Prefers stable weekly cadence and structured updates.\n\nBest move: fixed weekly summary + one priority ask per cycle.",
|
||||
},
|
||||
]);
|
||||
const contacts = ref<Contact[]>([]);
|
||||
|
||||
const calendarEvents = ref<CalendarEvent[]>([
|
||||
{
|
||||
id: "e1",
|
||||
title: "Anna follow-up",
|
||||
start: inMinutes(12),
|
||||
end: endAfter(inMinutes(12), 30),
|
||||
contact: "Anna Meyer",
|
||||
note: "Confirm decision date and next owner.",
|
||||
},
|
||||
{
|
||||
id: "e2",
|
||||
title: "Murat contract call",
|
||||
start: atOffset(2, 16, 30),
|
||||
end: endAfter(atOffset(2, 16, 30), 30),
|
||||
contact: "Murat Ali",
|
||||
note: "Lock legal owner and signing target.",
|
||||
},
|
||||
{
|
||||
id: "e3",
|
||||
title: "Ilya discovery",
|
||||
start: atOffset(3, 11, 0),
|
||||
end: endAfter(atOffset(3, 11, 0), 30),
|
||||
contact: "Ilya Petroff",
|
||||
note: "Qualify onboarding readiness.",
|
||||
},
|
||||
{
|
||||
id: "e4",
|
||||
title: "Internal sync",
|
||||
start: atOffset(0, 9, 0),
|
||||
end: endAfter(atOffset(0, 9, 0), 30),
|
||||
contact: "Team",
|
||||
note: "Align next actions across accounts.",
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
title: "BluePort intro",
|
||||
start: atOffset(4, 13, 0),
|
||||
end: endAfter(atOffset(4, 13, 0), 30),
|
||||
contact: "Carlos Rivera",
|
||||
note: "Check pilot scope and timeline.",
|
||||
},
|
||||
]);
|
||||
const calendarEvents = ref<CalendarEvent[]>([]);
|
||||
|
||||
const commItems = ref<CommItem[]>([
|
||||
{
|
||||
id: "m1",
|
||||
at: atOffset(0, 9, 20),
|
||||
contact: "Anna Meyer",
|
||||
channel: "Telegram",
|
||||
kind: "message",
|
||||
direction: "in",
|
||||
text: "Can you share final pricing today?",
|
||||
},
|
||||
{
|
||||
id: "m2",
|
||||
at: atOffset(0, 9, 22),
|
||||
contact: "Anna Meyer",
|
||||
channel: "Telegram",
|
||||
kind: "message",
|
||||
direction: "out",
|
||||
text: "Yes, sending after demo.",
|
||||
},
|
||||
{
|
||||
id: "m3",
|
||||
at: atOffset(0, 10, 0),
|
||||
contact: "Anna Meyer",
|
||||
channel: "Phone",
|
||||
kind: "call",
|
||||
direction: "out",
|
||||
text: "Quick sync call",
|
||||
duration: "14m",
|
||||
transcript: [
|
||||
"Anna: We can review pricing today, but I need one clear next step.",
|
||||
"You: Agreed. I will send the final version right after this call.",
|
||||
"Anna: Perfect. Add a follow-up tomorrow morning.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "m4",
|
||||
at: atOffset(-1, 15, 5),
|
||||
contact: "Murat Ali",
|
||||
channel: "WhatsApp",
|
||||
kind: "message",
|
||||
direction: "in",
|
||||
text: "Need to shift call by one day.",
|
||||
},
|
||||
{
|
||||
id: "m5",
|
||||
at: atOffset(-1, 15, 8),
|
||||
contact: "Murat Ali",
|
||||
channel: "WhatsApp",
|
||||
kind: "message",
|
||||
direction: "out",
|
||||
text: "Works. Let us do 12:00 tomorrow.",
|
||||
},
|
||||
{
|
||||
id: "m6",
|
||||
at: atOffset(-1, 18, 10),
|
||||
contact: "Murat Ali",
|
||||
channel: "Phone",
|
||||
kind: "call",
|
||||
direction: "in",
|
||||
text: "Contract call",
|
||||
duration: "9m",
|
||||
transcript: [
|
||||
"Murat: We can move forward if legal owner is confirmed today.",
|
||||
"You: Understood. I will lock owner and send the timeline.",
|
||||
"Murat: Then we target signature this week.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "m9",
|
||||
at: atOffset(-1, 13, 40),
|
||||
contact: "Murat Ali",
|
||||
channel: "Instagram",
|
||||
kind: "message",
|
||||
direction: "in",
|
||||
text: "Sent details in DM, can we align tomorrow?",
|
||||
},
|
||||
{
|
||||
id: "m7",
|
||||
at: atOffset(-1, 11, 12),
|
||||
contact: "Ilya Petroff",
|
||||
channel: "Email",
|
||||
kind: "message",
|
||||
direction: "in",
|
||||
text: "Interested in pilot, what are next steps?",
|
||||
},
|
||||
{
|
||||
id: "m8",
|
||||
at: atOffset(-1, 12, 0),
|
||||
contact: "Ilya Petroff",
|
||||
channel: "Phone",
|
||||
kind: "call",
|
||||
direction: "out",
|
||||
text: "Missed callback",
|
||||
duration: "-",
|
||||
transcript: [
|
||||
"No full transcript available. Call was not connected.",
|
||||
],
|
||||
},
|
||||
]);
|
||||
const commItems = ref<CommItem[]>([]);
|
||||
|
||||
const commPins = ref<CommPin[]>([
|
||||
{
|
||||
id: "cp-1",
|
||||
contact: "Anna Meyer",
|
||||
text: "First lock the decision date, then send the final offer.",
|
||||
},
|
||||
{
|
||||
id: "cp-2",
|
||||
contact: "Anna Meyer",
|
||||
text: "A short follow-up is needed no later than 30 minutes after the demo.",
|
||||
},
|
||||
{
|
||||
id: "cp-3",
|
||||
contact: "Murat Ali",
|
||||
text: "In every update, confirm the legal owner on the client side.",
|
||||
},
|
||||
{
|
||||
id: "cp-4",
|
||||
contact: "Ilya Petroff",
|
||||
text: "Work through a structured onboarding plan, not pricing first.",
|
||||
},
|
||||
]);
|
||||
const commPins = ref<CommPin[]>([]);
|
||||
|
||||
const deals = ref<Deal[]>([
|
||||
{
|
||||
id: "d1",
|
||||
contact: "Anna Meyer",
|
||||
title: "Nordline pilot",
|
||||
company: "Nordline GmbH",
|
||||
stage: "Negotiation",
|
||||
amount: "$28,000",
|
||||
nextStep: "Send post-demo offer within 30 minutes",
|
||||
summary: "Active thread, fast feedback, high close probability this week.",
|
||||
},
|
||||
{
|
||||
id: "d2",
|
||||
contact: "Murat Ali",
|
||||
title: "Connect annual",
|
||||
company: "Connect FZCO",
|
||||
stage: "Legal review",
|
||||
amount: "$74,000",
|
||||
nextStep: "Lock legal owner and expected sign date",
|
||||
summary: "Strong engagement in chat and calls, pending legal alignment.",
|
||||
},
|
||||
{
|
||||
id: "d3",
|
||||
contact: "Ilya Petroff",
|
||||
title: "Volta onboarding",
|
||||
company: "Volta Tech",
|
||||
stage: "Discovery",
|
||||
amount: "$12,000",
|
||||
nextStep: "Offer two onboarding slots for tomorrow",
|
||||
summary: "Early stage interest, needs concise onboarding plan.",
|
||||
},
|
||||
]);
|
||||
const deals = ref<Deal[]>([]);
|
||||
|
||||
const documents = ref<WorkspaceDocument[]>([
|
||||
{
|
||||
id: "doc-1",
|
||||
title: "Outbound cadence v1",
|
||||
type: "Regulation",
|
||||
owner: "Revenue Ops",
|
||||
scope: "All B2B accounts",
|
||||
updatedAt: atOffset(-1, 10, 0),
|
||||
summary: "Unified sequence for first touch, follow-up, and qualification.",
|
||||
body: "## Goal\nMove a new contact to the first qualified call within 5 business days.\n\n## Base sequence\n- Day 0: first message in the primary channel.\n- Day 1: short follow-up with one clear ask.\n- Day 3: second follow-up + alternate channel.\n- Day 5: final ping and move to \"later\".\n\n## Rules\n- Always keep one explicit next step in each message.\n- Avoid long text walls.\n- After each reply, update context in the contact card.",
|
||||
},
|
||||
{
|
||||
id: "doc-2",
|
||||
title: "Discovery call playbook",
|
||||
type: "Playbook",
|
||||
owner: "Sales Lead",
|
||||
scope: "Discovery calls",
|
||||
updatedAt: atOffset(-2, 12, 15),
|
||||
summary: "Call structure, mandatory questions, and outcome logging format.",
|
||||
body: "## Structure\n1. Context check (2 min)\n2. Current pain (8 min)\n3. Success criteria (6 min)\n4. Next step lock (4 min)\n\n## Mandatory outcomes\n- Confirmed business owner\n- Confirmed decision timeline\n- Confirmed next meeting date\n\n## Notes format\nAlways log: pain, impact, owner, ETA, risks.",
|
||||
},
|
||||
{
|
||||
id: "doc-3",
|
||||
title: "AI assistant operating policy",
|
||||
type: "Policy",
|
||||
owner: "Founders",
|
||||
scope: "AI recommendations and automations",
|
||||
updatedAt: atOffset(-3, 9, 40),
|
||||
summary: "What actions AI can suggest and what requires explicit approval.",
|
||||
body: "## Allowed without approval\n- Draft message suggestions\n- Calendar proposal suggestions\n- Conversation summaries\n\n## Requires explicit approval\n- Sending a message to an external contact\n- Creating a calendar event\n- Changing deal stage\n\n## Logging\nEvery AI action must leave a short trace in the feed.",
|
||||
},
|
||||
{
|
||||
id: "doc-4",
|
||||
title: "Post-call follow-up template",
|
||||
type: "Template",
|
||||
owner: "Enablement",
|
||||
scope: "Any completed client call",
|
||||
updatedAt: atOffset(-4, 16, 20),
|
||||
summary: "Template for short post-call follow-up with aligned actions.",
|
||||
body: "## Message template\nThanks for the call. Summary below:\n- What we aligned on\n- What remains open\n- Owner per action\n- Exact date for next sync\n\n## Quality bar\nThe client should understand the next step within 10 seconds.",
|
||||
},
|
||||
]);
|
||||
const documents = ref<WorkspaceDocument[]>([]);
|
||||
|
||||
type PilotMessage = {
|
||||
id: string;
|
||||
@@ -506,12 +174,83 @@ type PilotMessage = {
|
||||
const pilotMessages = ref<PilotMessage[]>([]);
|
||||
const pilotInput = ref("");
|
||||
const pilotSending = ref(false);
|
||||
const authMe = ref<{ user: { id: string; email: string; name: string }; team: { id: string; name: string } } | null>(
|
||||
null,
|
||||
);
|
||||
const loginEmail = ref("");
|
||||
const loginName = ref("");
|
||||
const loginTeamName = ref("My Team");
|
||||
const loginError = ref<string | null>(null);
|
||||
const loginBusy = ref(false);
|
||||
|
||||
async function loadPilotMessages() {
|
||||
const res = await $fetch<{ items: PilotMessage[] }>("/api/chat");
|
||||
pilotMessages.value = res.items ?? [];
|
||||
}
|
||||
|
||||
async function loadMe() {
|
||||
authMe.value = await $fetch("/api/auth/me");
|
||||
}
|
||||
|
||||
async function login() {
|
||||
loginError.value = null;
|
||||
loginBusy.value = true;
|
||||
try {
|
||||
await $fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: { email: loginEmail.value, name: loginName.value, teamName: loginTeamName.value },
|
||||
});
|
||||
await loadMe();
|
||||
await Promise.all([loadPilotMessages(), refreshCrmData()]);
|
||||
} catch (e: any) {
|
||||
loginError.value = e?.data?.message || e?.message || "Login failed";
|
||||
} finally {
|
||||
loginBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await $fetch("/api/auth/logout", { method: "POST" });
|
||||
authMe.value = null;
|
||||
}
|
||||
|
||||
async function useDemo() {
|
||||
await $fetch("/api/auth/demo", { method: "POST" });
|
||||
await loadMe();
|
||||
await Promise.all([loadPilotMessages(), refreshCrmData()]);
|
||||
}
|
||||
|
||||
async function refreshCrmData() {
|
||||
const [contactsRes, commRes, calRes, dealsRes, feedRes, pinsRes, docsRes] = await Promise.all([
|
||||
$fetch<{ items: Contact[] }>("/api/contacts"),
|
||||
$fetch<{ items: CommItem[] }>("/api/communications"),
|
||||
$fetch<{ items: CalendarEvent[] }>("/api/calendar"),
|
||||
$fetch<{ items: Deal[] }>("/api/deals"),
|
||||
$fetch<{ items: FeedCard[] }>("/api/feed"),
|
||||
$fetch<{ items: CommPin[] }>("/api/pins"),
|
||||
$fetch<{ items: WorkspaceDocument[] }>("/api/documents"),
|
||||
]);
|
||||
|
||||
contacts.value = contactsRes.items ?? [];
|
||||
commItems.value = commRes.items ?? [];
|
||||
calendarEvents.value = calRes.items ?? [];
|
||||
deals.value = dealsRes.items ?? [];
|
||||
feedCards.value = feedRes.items ?? [];
|
||||
commPins.value = pinsRes.items ?? [];
|
||||
documents.value = docsRes.items ?? [];
|
||||
|
||||
// Derive channels per contact from communication items.
|
||||
const byName = new Map<string, Set<string>>();
|
||||
for (const item of commItems.value) {
|
||||
if (!byName.has(item.contact)) byName.set(item.contact, new Set());
|
||||
byName.get(item.contact)?.add(item.channel);
|
||||
}
|
||||
contacts.value = contacts.value.map((c) => ({
|
||||
...c,
|
||||
channels: Array.from(byName.get(c.name) ?? []),
|
||||
}));
|
||||
}
|
||||
|
||||
async function sendPilotMessage() {
|
||||
const text = pilotInput.value.trim();
|
||||
if (!text || pilotSending.value) return;
|
||||
@@ -527,7 +266,9 @@ async function sendPilotMessage() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPilotMessages();
|
||||
loadMe()
|
||||
.then(() => Promise.all([loadPilotMessages(), refreshCrmData()]))
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
const calendarView = ref<CalendarView>("month");
|
||||
@@ -1179,7 +920,8 @@ function openMessageFromContact(channel: CommItem["channel"]) {
|
||||
selectedCommChannel.value = channel;
|
||||
}
|
||||
|
||||
function executeFeedAction(key: FeedCard["proposal"]["key"]) {
|
||||
async function executeFeedAction(card: FeedCard) {
|
||||
const key = card.proposal.key;
|
||||
if (key === "create_followup") {
|
||||
const start = new Date();
|
||||
start.setMinutes(start.getMinutes() + 30);
|
||||
@@ -1187,54 +929,62 @@ function executeFeedAction(key: FeedCard["proposal"]["key"]) {
|
||||
const end = new Date(start);
|
||||
end.setMinutes(end.getMinutes() + 30);
|
||||
|
||||
calendarEvents.value.unshift({
|
||||
id: makeId("e"),
|
||||
title: "Follow-up: Anna",
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
contact: "Anna Meyer",
|
||||
note: "Created from feed action.",
|
||||
const res = await $fetch<{ item: CalendarEvent }>("/api/calendar", {
|
||||
method: "POST",
|
||||
body: {
|
||||
title: `Follow-up: ${card.contact.split(" ")[0] ?? "Contact"}`,
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
contact: card.contact,
|
||||
note: "Created from feed action.",
|
||||
status: "planned",
|
||||
},
|
||||
});
|
||||
calendarEvents.value = [res.item, ...calendarEvents.value];
|
||||
|
||||
selectedDateKey.value = dayKey(start);
|
||||
calendarCursor.value = new Date(start.getFullYear(), start.getMonth(), 1);
|
||||
selectedTab.value = "communications";
|
||||
peopleLeftMode.value = "calendar";
|
||||
return `Event created: Follow-up: Anna · ${formatDay(start.toISOString())} ${formatTime(start.toISOString())}`;
|
||||
return `Event created: Follow-up · ${formatDay(start.toISOString())} ${formatTime(start.toISOString())} · ${card.contact}`;
|
||||
}
|
||||
|
||||
if (key === "open_comm") {
|
||||
openCommunicationThread("Murat Ali");
|
||||
return "Opened Murat Ali communication thread.";
|
||||
openCommunicationThread(card.contact);
|
||||
return `Opened ${card.contact} communication thread.`;
|
||||
}
|
||||
|
||||
if (key === "call") {
|
||||
commItems.value.push({
|
||||
id: makeId("m"),
|
||||
at: new Date().toISOString(),
|
||||
contact: "Murat Ali",
|
||||
channel: "Phone",
|
||||
kind: "call",
|
||||
direction: "out",
|
||||
text: "Call started from feed",
|
||||
duration: "00:00",
|
||||
await $fetch("/api/communications", {
|
||||
method: "POST",
|
||||
body: {
|
||||
contact: card.contact,
|
||||
channel: "Phone",
|
||||
kind: "call",
|
||||
direction: "out",
|
||||
text: "Call started from feed",
|
||||
durationSec: 0,
|
||||
},
|
||||
});
|
||||
openCommunicationThread("Murat Ali");
|
||||
return "Call event created and Murat Ali chat opened.";
|
||||
await refreshCrmData();
|
||||
openCommunicationThread(card.contact);
|
||||
return `Call event created and ${card.contact} chat opened.`;
|
||||
}
|
||||
|
||||
if (key === "draft_message") {
|
||||
commItems.value.push({
|
||||
id: makeId("m"),
|
||||
at: new Date().toISOString(),
|
||||
contact: "Ilya Petroff",
|
||||
channel: "Email",
|
||||
kind: "message",
|
||||
direction: "out",
|
||||
text: "Draft: onboarding plan + two slots for tomorrow.",
|
||||
await $fetch("/api/communications", {
|
||||
method: "POST",
|
||||
body: {
|
||||
contact: card.contact,
|
||||
channel: "Email",
|
||||
kind: "message",
|
||||
direction: "out",
|
||||
text: "Draft: onboarding plan + two slots for tomorrow.",
|
||||
},
|
||||
});
|
||||
openCommunicationThread("Ilya Petroff");
|
||||
return "Draft message added to Ilya Petroff communications.";
|
||||
await refreshCrmData();
|
||||
openCommunicationThread(card.contact);
|
||||
return `Draft message added to ${card.contact} communications.`;
|
||||
}
|
||||
|
||||
if (key === "run_summary") {
|
||||
@@ -1242,32 +992,38 @@ function executeFeedAction(key: FeedCard["proposal"]["key"]) {
|
||||
}
|
||||
|
||||
if (key === "prepare_question") {
|
||||
commItems.value.push({
|
||||
id: makeId("m"),
|
||||
at: new Date().toISOString(),
|
||||
contact: "Anna Meyer",
|
||||
channel: "Telegram",
|
||||
kind: "message",
|
||||
direction: "out",
|
||||
text: "Draft: can you confirm your decision date for this cycle?",
|
||||
await $fetch("/api/communications", {
|
||||
method: "POST",
|
||||
body: {
|
||||
contact: card.contact,
|
||||
channel: "Telegram",
|
||||
kind: "message",
|
||||
direction: "out",
|
||||
text: "Draft: can you confirm your decision date for this cycle?",
|
||||
},
|
||||
});
|
||||
openCommunicationThread("Anna Meyer");
|
||||
return "Question about decision date added to Anna Meyer chat.";
|
||||
await refreshCrmData();
|
||||
openCommunicationThread(card.contact);
|
||||
return `Question about decision date added to ${card.contact} chat.`;
|
||||
}
|
||||
|
||||
return "Action completed.";
|
||||
}
|
||||
|
||||
function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") {
|
||||
async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") {
|
||||
card.decision = decision;
|
||||
|
||||
if (decision === "rejected") {
|
||||
card.decisionNote = "Rejected. Nothing created.";
|
||||
const note = "Rejected. Nothing created.";
|
||||
card.decisionNote = note;
|
||||
await $fetch(`/api/feed/${card.id}`, { method: "PUT", body: { decision: "rejected", decisionNote: note } });
|
||||
pushPilotNote(`[${card.contact}] recommendation rejected: ${card.proposal.title}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = executeFeedAction(card.proposal.key);
|
||||
const result = await executeFeedAction(card);
|
||||
card.decisionNote = result;
|
||||
await $fetch(`/api/feed/${card.id}`, { method: "PUT", body: { decision: "accepted", decisionNote: result } });
|
||||
pushPilotNote(`[${card.contact}] ${result}`);
|
||||
}
|
||||
|
||||
@@ -1278,56 +1034,84 @@ function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") {
|
||||
<div class="grid gap-3 lg:grid-cols-12 lg:gap-4">
|
||||
<aside class="card min-h-0 border border-base-300 bg-base-100 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-3 md:p-4">
|
||||
<div class="mb-2">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-base-content/70">Pilot</h2>
|
||||
<div class="mb-2 flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-base-content/70">Pilot</h2>
|
||||
<p v-if="authMe" class="mt-1 text-xs text-base-content/60">
|
||||
{{ authMe.team.name }} · {{ authMe.user.name }}
|
||||
</p>
|
||||
</div>
|
||||
<button v-if="authMe" class="btn btn-ghost btn-xs" @click="logout">Logout</button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 space-y-2 overflow-y-auto rounded-xl border border-base-300 p-2">
|
||||
<div
|
||||
v-for="message in pilotMessages"
|
||||
:key="message.id"
|
||||
class="chat"
|
||||
:class="message.role === 'assistant' ? 'chat-start' : 'chat-end'"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="chat-bubble text-sm" :class="message.role === 'assistant' ? 'chat-bubble-neutral' : ''">
|
||||
{{ message.text }}
|
||||
</div>
|
||||
<div v-if="!authMe" class="space-y-3">
|
||||
<div class="rounded-xl border border-base-300 bg-base-100 p-3">
|
||||
<p class="text-sm font-semibold">Login</p>
|
||||
<p class="mt-1 text-xs text-base-content/60">MVP: email + name. Потом подключим нормальную auth.</p>
|
||||
|
||||
<details
|
||||
v-if="message.role === 'assistant' && ((message.plan && message.plan.length) || (message.tools && message.tools.length))"
|
||||
class="rounded-lg border border-base-300 bg-base-100 p-2 text-xs"
|
||||
>
|
||||
<summary class="cursor-pointer select-none font-semibold text-base-content/70">Plan & tools</summary>
|
||||
<div class="mt-2 space-y-2">
|
||||
<div v-if="message.plan && message.plan.length">
|
||||
<div class="font-semibold text-base-content/70">Plan</div>
|
||||
<ul class="list-disc pl-4">
|
||||
<li v-for="(step, idx) in message.plan" :key="`plan-${message.id}-${idx}`">{{ step }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="message.tools && message.tools.length">
|
||||
<div class="font-semibold text-base-content/70">Tools</div>
|
||||
<ul class="list-disc pl-4">
|
||||
<li v-for="(tool, idx) in message.tools" :key="`tools-${message.id}-${idx}`">{{ tool }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="mt-3 space-y-2">
|
||||
<input v-model="loginEmail" type="email" class="input input-bordered input-sm w-full" placeholder="Email" />
|
||||
<input v-model="loginName" type="text" class="input input-bordered input-sm w-full" placeholder="Name" />
|
||||
<input v-model="loginTeamName" type="text" class="input input-bordered input-sm w-full" placeholder="Team name" />
|
||||
<p v-if="loginError" class="text-xs text-error">{{ loginError }}</p>
|
||||
<button class="btn btn-sm w-full" :disabled="loginBusy" @click="login">
|
||||
{{ loginBusy ? "Logging in..." : "Login" }}
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm w-full" :disabled="loginBusy" @click="useDemo">
|
||||
Use demo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex gap-2">
|
||||
<input
|
||||
v-model="pilotInput"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="Write a message..."
|
||||
@keyup.enter="sendPilotMessage"
|
||||
>
|
||||
<button class="btn btn-sm" @click="sendPilotMessage">Send</button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="min-h-0 flex-1 space-y-2 overflow-y-auto rounded-xl border border-base-300 p-2">
|
||||
<div
|
||||
v-for="message in pilotMessages"
|
||||
:key="message.id"
|
||||
class="chat"
|
||||
:class="message.role === 'assistant' ? 'chat-start' : 'chat-end'"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="chat-bubble text-sm" :class="message.role === 'assistant' ? 'chat-bubble-neutral' : ''">
|
||||
{{ message.text }}
|
||||
</div>
|
||||
|
||||
<details
|
||||
v-if="message.role === 'assistant' && ((message.plan && message.plan.length) || (message.tools && message.tools.length))"
|
||||
class="rounded-lg border border-base-300 bg-base-100 p-2 text-xs"
|
||||
>
|
||||
<summary class="cursor-pointer select-none font-semibold text-base-content/70">Plan & tools</summary>
|
||||
<div class="mt-2 space-y-2">
|
||||
<div v-if="message.plan && message.plan.length">
|
||||
<div class="font-semibold text-base-content/70">Plan</div>
|
||||
<ul class="list-disc pl-4">
|
||||
<li v-for="(step, idx) in message.plan" :key="`plan-${message.id}-${idx}`">{{ step }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="message.tools && message.tools.length">
|
||||
<div class="font-semibold text-base-content/70">Tools</div>
|
||||
<ul class="list-disc pl-4">
|
||||
<li v-for="(tool, idx) in message.tools" :key="`tools-${message.id}-${idx}`">{{ tool }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex gap-2">
|
||||
<input
|
||||
v-model="pilotInput"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="Write a message..."
|
||||
@keyup.enter="sendPilotMessage"
|
||||
>
|
||||
<button class="btn btn-sm" :disabled="pilotSending" @click="sendPilotMessage">Send</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user