diff --git a/frontend/src/components/AgAgentPanel.jsx b/frontend/src/components/AgAgentPanel.jsx index 04a29f0..8927f0b 100644 --- a/frontend/src/components/AgAgentPanel.jsx +++ b/frontend/src/components/AgAgentPanel.jsx @@ -2,14 +2,15 @@ import { useState } from "react"; import { Calendar, Check, + ChevronDown, ChevronRight, FileSearch, ListChecks, + Mail, Paperclip, PenLine, Receipt, Send, - Star, } from "lucide-react"; import AgentInsight from "./AgentInsight"; import AgentSpark from "./AgentSpark"; @@ -19,8 +20,8 @@ import { inboxContext } from "../utils/inboxSignals"; const ICON = { size: 14, strokeWidth: 1.75 }; const cards = [ - { key: "important", label: "Important emails", captionKey: "importantFromClients", captionSuffix: "from clients", icon: Star, tone: "rose" }, - { key: "actionItems", label: "Action items", hint: "Need response", icon: ListChecks, tone: "blue" }, + { key: "important", label: "Important emails", captionKey: "importantFromClients", captionSuffix: "from clients", icon: Mail, tone: "rose" }, + { key: "actionItems", label: "Action items", hint: "Need your response", icon: ListChecks, tone: "blue" }, { key: "meetings", label: "Meetings mentioned", hint: "In your emails", icon: Calendar, tone: "green" }, { key: "invoices", label: "Invoices & bills", hint: "This week", icon: Receipt, tone: "amber" }, ]; @@ -42,8 +43,11 @@ export default function AgAgentPanel({ }) { const { stats, needsReply, today, deadlines } = inboxContext(emails); const [agentQuery, setAgentQuery] = useState(""); + const [panelOpen, setPanelOpen] = useState(true); const firstName = account?.first_name || "there"; - const hasSignals = stats.important + stats.actionItems + stats.meetings + stats.invoices > 0; + const activeCards = cards.filter((card) => (stats[card.key] || 0) > 0); + const hasContext = needsReply.length > 0 || today.length > 0 || deadlines.length > 0; + const hasSignals = activeCards.length > 0 || hasContext; function submitAgentQuery(event) { event.preventDefault(); @@ -57,57 +61,68 @@ export default function AgAgentPanel({
-
+

AG Agent

Your email co-pilot

+
- {!insight && hasSignals && ( -
- {greeting(firstName)} - Here's what needs your attention today. -
- )} + {panelOpen && ( +
+ {!insight && hasSignals && ( +
+ {greeting(firstName)} + Here's what needs your attention today. +
+ )} - {!insight && !hasSignals && ( -
-

You're all caught up.

-

No urgent messages or outstanding actions.

- Inbox is quiet right now. -
- )} + {!insight && !hasSignals && ( +
+

You're all caught up.

+

No urgent messages or outstanding actions.

+ Inbox is quiet right now. +
+ )} - {!insight && hasSignals && ( -
- {cards.map((card) => { - const Icon = card.icon; - const count = stats[card.key] || 0; - const caption = card.captionKey && stats[card.captionKey] - ? `${stats[card.captionKey]} ${card.captionSuffix}` - : card.hint; - return ( - - ); - })} -
- )} + {!insight && activeCards.length > 0 && ( +
+ {activeCards.map((card) => { + const Icon = card.icon; + const count = stats[card.key] || 0; + const caption = card.captionKey && stats[card.captionKey] + ? `${stats[card.captionKey]} ${card.captionSuffix}` + : card.hint; + return ( + + ); + })} +
+ )} - {!insight && focus && hasSignals &&

{focus}

} + {!insight && focus && hasSignals &&

{focus}

} -
+

Quick actions

+
-
- {!insight && today.length > 0 && ( + {!insight && today.length > 0 && (

Today's agenda

@@ -148,10 +163,10 @@ export default function AgAgentPanel({ ))} -
- )} +
+ )} - {!insight && needsReply.length > 0 && ( + {!insight && needsReply.length > 0 && (

Needs attention

-
- )} + + )} - {!insight && deadlines.length > 0 && ( + {!insight && deadlines.length > 0 && (

Deadlines

-
- )} + + )} -
+ setAgentQuery(event.target.value)} @@ -196,8 +211,10 @@ export default function AgAgentPanel({ -
+ + + + )} ); } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index b0a7c5f..d1e682b 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -591,9 +591,14 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; } .mail-label-pink { background:#fceeff; color:#8b5ca8; } .mailbox-rail { min-width:0; display:flex; flex-direction:column; gap:0; position:sticky; top:var(--space-3); align-self:start; } .agent-panel { padding:var(--space-5) var(--space-4) var(--space-4); border:1px solid var(--border-normal); border-bottom:0; border-radius:var(--radius-xl) var(--radius-xl) 0 0; background:var(--surface); box-shadow:0 2px 6px rgba(20,20,30,0.04),0 14px 36px rgba(20,20,30,0.07); } -.agent-panel-head { margin-bottom:var(--space-3); display:flex; align-items:center; gap:var(--space-3); } -.agent-panel-mark { width:36px; height:36px; flex-shrink:0; display:grid; place-items:center; border-radius:var(--radius-md); background:linear-gradient(180deg,#faf4ff 0%,#f3ebff 100%); color:var(--agent); font-size:15px; box-shadow:inset 0 0 0 1px #ebe2ff; } +.agent-panel-head { margin-bottom:var(--space-3); display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:var(--space-3); } +.agent-panel-mark { width:36px; height:36px; flex-shrink:0; display:grid; place-items:center; border-radius:50%; background:linear-gradient(180deg,#ffb199 0%,#ff7f6e 100%); color:#fff; font-size:15px; box-shadow:0 4px 12px rgba(255,106,82,0.28); } +.agent-panel-title { min-width:0; } .agent-panel-head h2 { margin:0; font:700 15px "Manrope"; letter-spacing:-.25px; color:#1f1b28; } +.agent-panel-toggle { width:28px; height:28px; display:grid; place-items:center; border:0; border-radius:var(--radius-xs); background:transparent; color:#9a949e; cursor:pointer; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); } +.agent-panel-toggle:hover { background:#f5f3f0; color:#5a5562; } +.agent-panel-toggle .open { transform:rotate(180deg); } +.agent-panel-body { display:grid; gap:0; animation:agentReveal var(--motion-slow) var(--ease-standard); } .agent-panel-hello { margin:0 0 var(--space-4); display:grid; gap:var(--space-1); } .agent-panel-hello strong { font:650 15px "Manrope"; letter-spacing:-.2px; color:#1f1b28; } .agent-panel-hello span { color:#6f6a64; font-size:var(--text-meta); line-height:var(--line-content); } @@ -602,7 +607,11 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; } .agent-stat-grid { margin-bottom:var(--space-4); display:grid; grid-template-columns:1fr 1fr; gap:var(--space-2); } .agent-stat { min-height:92px; padding:var(--space-3) 11px; display:grid; align-content:start; gap:3px; border:1px solid transparent; border-radius:var(--radius-lg); text-align:left; cursor:pointer; transition:transform var(--motion-fast) var(--ease-standard),box-shadow var(--motion-fast) var(--ease-standard); } .agent-stat:hover { transform:translateY(-1px); box-shadow:0 4px 12px rgba(20,20,30,0.06); } -.agent-stat-icon { width:26px; height:26px; margin-bottom:2px; display:grid; place-items:center; border-radius:8px; background:#ffffff88; } +.agent-stat-icon { width:28px; height:28px; margin-bottom:4px; display:grid; place-items:center; border-radius:8px; background:#fff; box-shadow:0 1px 2px rgba(20,20,30,0.05); } +.agent-stat-rose .agent-stat-icon { color:#e15b45; } +.agent-stat-blue .agent-stat-icon { color:#4d74c4; } +.agent-stat-green .agent-stat-icon { color:#3d8a5d; } +.agent-stat-amber .agent-stat-icon { color:#8b6fd4; } .agent-stat strong { font:700 24px "Manrope"; letter-spacing:-.7px; color:#241f2b; line-height:1; } .agent-stat span { color:#3d3944; font-size:11px; font-weight:600; line-height:1.25; } .agent-stat small { color:#7d7872; font-size:10px; line-height:1.25; } @@ -625,7 +634,10 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; } .agent-quick-btn-success:hover { background:#f3faf5; border-color:#c5e5d0; } .agent-attention-avatar { width:30px!important; height:30px!important; font-size:10px!important; } .agent-attention-copy { min-width:0; display:grid; gap:2px; text-align:left; } -.agent-attention-list button { width:100%; padding:10px 0; display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:var(--space-2); border:0; background:transparent; text-align:left; cursor:pointer; } +.agent-attention-list li { border-bottom:1px solid var(--border-subtle); } +.agent-attention-list li:last-child { border-bottom:0; } +.agent-attention-list button { width:100%; padding:10px 0; display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:var(--space-2); border:0; background:transparent; text-align:left; cursor:pointer; transition:background var(--motion-fast) var(--ease-standard); } +.agent-attention-list button:hover { background:#faf9fc; border-radius:var(--radius-sm); } .agent-attention-list em { color:#a39ea8; font-size:10px; font-style:normal; font-weight:600; } .agent-caught-up { margin:0 0 var(--space-4); padding:14px var(--space-3); border-radius:var(--radius-lg); background:linear-gradient(180deg,#edf8f1 0%,#f5faf7 100%); border:1px solid #c8e6d4; color:#5f5a54; box-shadow:inset 0 1px 0 #ffffffaa; } .agent-caught-up-lead { margin:0 0 6px; display:flex; align-items:center; gap:var(--space-2); color:#2a7a4f; font-size:var(--text-meta); font-weight:700; } @@ -640,6 +652,7 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; } .agent-agenda-row:hover { background:#f5f4f8; } .agent-agenda-bar { width:4px; height:100%; min-height:40px; border-radius:var(--radius-pill); background:#3d8a5d; } .agent-agenda-row.tone-blue .agent-agenda-bar { background:#4d74c4; } +.agent-agenda-row.tone-rose .agent-agenda-bar { background:var(--coral); } .agent-agenda-row.tone-amber .agent-agenda-bar { background:#b7892d; } .agent-agenda-row time { display:block; color:#8d8792; font-size:10px; font-weight:600; } .agent-agenda-row strong { display:block; color:#2a2530; font-size:var(--text-meta); font-weight:600; line-height:1.3; } diff --git a/frontend/src/utils/inboxSignals.js b/frontend/src/utils/inboxSignals.js index c3c425c..d9afc3e 100644 --- a/frontend/src/utils/inboxSignals.js +++ b/frontend/src/utils/inboxSignals.js @@ -24,6 +24,9 @@ export function emailCategory(email) { return "Primary"; } +const PRIORITY_LABELS = new Set(["Important", "Clients", "Projects", "Finance", "HR"]); +const ACTION_LABELS = new Set(["Clients", "Important", "Projects", "Finance", "HR", "AI"]); + export function emailLabels(email) { const text = emailSearchText(email); const labels = []; @@ -48,7 +51,7 @@ export function emailLabels(email) { if (/ai|agent|llm|gpt|model/.test(text)) { labels.push({ name: "AI", tone: "pink" }); } - if (/domain|dns|mx|spf|dkim/.test(text)) { + if (/domain|dns|mx|spf|dkim|\.env|environment variable/.test(text)) { labels.push({ name: "Domains", tone: "amber" }); } if (/linkedin|twitter|facebook|instagram|social/.test(text)) { @@ -81,19 +84,37 @@ export function categoryCounts(emails) { return counts; } +function hasPriorityLabel(email) { + return emailLabels(email).some((label) => PRIORITY_LABELS.has(label.name)); +} + +function hasActionLabel(email) { + return emailLabels(email).some((label) => ACTION_LABELS.has(label.name)); +} + +function looksActionable(email) { + const text = emailSearchText(email); + return /please|action|reply|review|attached|confirm|need|waiting|\?|follow up|response/.test(text) + || attachmentCount(email) > 0 + || /https?:\/\//.test(text); +} + export function inboxIntelligence(emails) { const unread = emails.filter((email) => email.is_read === false); const important = emails.filter((email) => - email.is_starred || emailLabels(email).some((label) => label.name === "Important") || email.is_read === false, + email.is_starred + || email.is_read === false + || hasPriorityLabel(email) + || emailLabels(email).length > 0, ); const actionItems = emails.filter((email) => - /please|action|reply|review|attached|confirm|need/.test(emailSearchText(email)), + looksActionable(email) || hasActionLabel(email) || email.is_read === false, ); const meetings = emails.filter((email) => - /meet|meeting|calendar|schedule|call|zoom|interview/.test(emailSearchText(email)), + /meet|meeting|calendar|schedule|call|zoom|interview|sync|demo|standup/.test(emailSearchText(email)), ); const invoices = emails.filter((email) => - /invoice|bill|payment|receipt/.test(emailSearchText(email)), + /invoice|bill|payment|receipt|purchase order|\bpo\b/.test(emailSearchText(email)), ); return { unread: unread.length, @@ -172,7 +193,10 @@ export function inboxContext(emails) { const needsReply = emails .filter((email) => - /please|reply|respond|confirm|review|attached|need your|waiting|action required/.test( + email.is_read === false + || looksActionable(email) + || hasActionLabel(email) + || /please|reply|respond|confirm|review|attached|need your|waiting|action required/.test( emailSearchText(email), ), ) @@ -197,7 +221,13 @@ export function inboxContext(emails) { if (!time) return; const label = (email.subject || "Meeting").trim().slice(0, 42); const venue = extractVenue(text); - const tone = /invoice|payment|bill/.test(text) ? "amber" : /client|demo/.test(text) ? "blue" : "green"; + const tone = /invoice|payment|bill|purchase order|\bpo\b/.test(text) + ? "amber" + : /sync|project/.test(text) + ? "rose" + : /client|demo/.test(text) + ? "blue" + : "green"; if (!today.some((item) => item.time === time && item.label === label)) { today.push({ time, label, venue, tone }); }