right side
This commit is contained in:
parent
5233481f06
commit
7fb9e0d5d4
3 changed files with 128 additions and 68 deletions
|
|
@ -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({
|
|||
<section className="agent-panel" aria-label="AG Agent">
|
||||
<header className="agent-panel-head">
|
||||
<span className="agent-panel-mark" aria-hidden="true"><AgentSpark /></span>
|
||||
<div>
|
||||
<div className="agent-panel-title">
|
||||
<h2>AG Agent</h2>
|
||||
<p>Your email co-pilot</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-panel-toggle"
|
||||
aria-expanded={panelOpen}
|
||||
aria-label={panelOpen ? "Collapse agent panel" : "Expand agent panel"}
|
||||
onClick={() => setPanelOpen((open) => !open)}
|
||||
>
|
||||
<ChevronDown size={16} strokeWidth={1.75} className={panelOpen ? "open" : ""} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<AgentInsight insight={insight} />
|
||||
|
||||
{!insight && hasSignals && (
|
||||
<div className="agent-panel-hello">
|
||||
<strong>{greeting(firstName)}</strong>
|
||||
<span>Here's what needs your attention today.</span>
|
||||
</div>
|
||||
)}
|
||||
{panelOpen && (
|
||||
<div className="agent-panel-body">
|
||||
{!insight && hasSignals && (
|
||||
<div className="agent-panel-hello">
|
||||
<strong>{greeting(firstName)}</strong>
|
||||
<span>Here's what needs your attention today.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!insight && !hasSignals && (
|
||||
<div className="agent-caught-up">
|
||||
<p className="agent-caught-up-lead"><Check size={15} strokeWidth={2} /> You're all caught up.</p>
|
||||
<p>No urgent messages or outstanding actions.</p>
|
||||
<small>Inbox is quiet right now.</small>
|
||||
</div>
|
||||
)}
|
||||
{!insight && !hasSignals && (
|
||||
<div className="agent-caught-up">
|
||||
<p className="agent-caught-up-lead"><Check size={15} strokeWidth={2} /> You're all caught up.</p>
|
||||
<p>No urgent messages or outstanding actions.</p>
|
||||
<small>Inbox is quiet right now.</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!insight && hasSignals && (
|
||||
<div className="agent-stat-grid">
|
||||
{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 (
|
||||
<button
|
||||
key={card.key}
|
||||
type="button"
|
||||
className={`agent-stat agent-stat-${card.tone}`}
|
||||
onClick={() => onQuickAction(card.key)}
|
||||
>
|
||||
<span className="agent-stat-icon"><Icon {...ICON} /></span>
|
||||
<strong>{count}</strong>
|
||||
<span>{card.label}</span>
|
||||
<small>{caption}</small>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!insight && activeCards.length > 0 && (
|
||||
<div className="agent-stat-grid">
|
||||
{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 (
|
||||
<button
|
||||
key={card.key}
|
||||
type="button"
|
||||
className={`agent-stat agent-stat-${card.tone}`}
|
||||
onClick={() => onQuickAction(card.key)}
|
||||
>
|
||||
<span className="agent-stat-icon"><Icon {...ICON} /></span>
|
||||
<strong>{count}</strong>
|
||||
<span>{card.label}</span>
|
||||
<small>{caption}</small>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!insight && focus && hasSignals && <p className="agent-panel-note">{focus}</p>}
|
||||
{!insight && focus && hasSignals && <p className="agent-panel-note">{focus}</p>}
|
||||
|
||||
<div className="agent-quick">
|
||||
<div className="agent-quick">
|
||||
<h3>Quick actions</h3>
|
||||
<div>
|
||||
<button type="button" className="agent-quick-btn agent-quick-btn-agent" onClick={() => onQuickAction("summarize")}>
|
||||
|
|
@ -126,10 +141,10 @@ export default function AgAgentPanel({
|
|||
<span className="agent-quick-icon"><ListChecks {...ICON} /></span>
|
||||
<span>Show action items</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!insight && today.length > 0 && (
|
||||
{!insight && today.length > 0 && (
|
||||
<section className="agent-context-block agent-agenda">
|
||||
<div className="agent-context-head">
|
||||
<h3>Today's agenda</h3>
|
||||
|
|
@ -148,10 +163,10 @@ export default function AgAgentPanel({
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!insight && needsReply.length > 0 && (
|
||||
{!insight && needsReply.length > 0 && (
|
||||
<section className="agent-context-block">
|
||||
<h3>Needs attention</h3>
|
||||
<ul className="agent-attention-list">
|
||||
|
|
@ -168,10 +183,10 @@ export default function AgAgentPanel({
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!insight && deadlines.length > 0 && (
|
||||
{!insight && deadlines.length > 0 && (
|
||||
<section className="agent-context-block agent-deadlines">
|
||||
<h3>Deadlines</h3>
|
||||
<ul>
|
||||
|
|
@ -182,10 +197,10 @@ export default function AgAgentPanel({
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<form className="agent-ask-input" onSubmit={submitAgentQuery}>
|
||||
<form className="agent-ask-input" onSubmit={submitAgentQuery}>
|
||||
<input
|
||||
value={agentQuery}
|
||||
onChange={(event) => setAgentQuery(event.target.value)}
|
||||
|
|
@ -196,8 +211,10 @@ export default function AgAgentPanel({
|
|||
</button>
|
||||
<button type="submit" className="agent-ask-send" aria-label="Send to agent" disabled={!agentQuery.trim()}>
|
||||
<Send size={15} strokeWidth={1.75} />
|
||||
</button>
|
||||
</form>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue