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 {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
Check,
|
Check,
|
||||||
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
FileSearch,
|
FileSearch,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
|
Mail,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
PenLine,
|
PenLine,
|
||||||
Receipt,
|
Receipt,
|
||||||
Send,
|
Send,
|
||||||
Star,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import AgentInsight from "./AgentInsight";
|
import AgentInsight from "./AgentInsight";
|
||||||
import AgentSpark from "./AgentSpark";
|
import AgentSpark from "./AgentSpark";
|
||||||
|
|
@ -19,8 +20,8 @@ import { inboxContext } from "../utils/inboxSignals";
|
||||||
const ICON = { size: 14, strokeWidth: 1.75 };
|
const ICON = { size: 14, strokeWidth: 1.75 };
|
||||||
|
|
||||||
const cards = [
|
const cards = [
|
||||||
{ key: "important", label: "Important emails", captionKey: "importantFromClients", captionSuffix: "from clients", icon: Star, tone: "rose" },
|
{ key: "important", label: "Important emails", captionKey: "importantFromClients", captionSuffix: "from clients", icon: Mail, tone: "rose" },
|
||||||
{ key: "actionItems", label: "Action items", hint: "Need response", icon: ListChecks, tone: "blue" },
|
{ 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: "meetings", label: "Meetings mentioned", hint: "In your emails", icon: Calendar, tone: "green" },
|
||||||
{ key: "invoices", label: "Invoices & bills", hint: "This week", icon: Receipt, tone: "amber" },
|
{ 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 { stats, needsReply, today, deadlines } = inboxContext(emails);
|
||||||
const [agentQuery, setAgentQuery] = useState("");
|
const [agentQuery, setAgentQuery] = useState("");
|
||||||
|
const [panelOpen, setPanelOpen] = useState(true);
|
||||||
const firstName = account?.first_name || "there";
|
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) {
|
function submitAgentQuery(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -57,14 +61,25 @@ export default function AgAgentPanel({
|
||||||
<section className="agent-panel" aria-label="AG Agent">
|
<section className="agent-panel" aria-label="AG Agent">
|
||||||
<header className="agent-panel-head">
|
<header className="agent-panel-head">
|
||||||
<span className="agent-panel-mark" aria-hidden="true"><AgentSpark /></span>
|
<span className="agent-panel-mark" aria-hidden="true"><AgentSpark /></span>
|
||||||
<div>
|
<div className="agent-panel-title">
|
||||||
<h2>AG Agent</h2>
|
<h2>AG Agent</h2>
|
||||||
<p>Your email co-pilot</p>
|
<p>Your email co-pilot</p>
|
||||||
</div>
|
</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>
|
</header>
|
||||||
|
|
||||||
<AgentInsight insight={insight} />
|
<AgentInsight insight={insight} />
|
||||||
|
|
||||||
|
{panelOpen && (
|
||||||
|
<div className="agent-panel-body">
|
||||||
{!insight && hasSignals && (
|
{!insight && hasSignals && (
|
||||||
<div className="agent-panel-hello">
|
<div className="agent-panel-hello">
|
||||||
<strong>{greeting(firstName)}</strong>
|
<strong>{greeting(firstName)}</strong>
|
||||||
|
|
@ -80,9 +95,9 @@ export default function AgAgentPanel({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!insight && hasSignals && (
|
{!insight && activeCards.length > 0 && (
|
||||||
<div className="agent-stat-grid">
|
<div className="agent-stat-grid">
|
||||||
{cards.map((card) => {
|
{activeCards.map((card) => {
|
||||||
const Icon = card.icon;
|
const Icon = card.icon;
|
||||||
const count = stats[card.key] || 0;
|
const count = stats[card.key] || 0;
|
||||||
const caption = card.captionKey && stats[card.captionKey]
|
const caption = card.captionKey && stats[card.captionKey]
|
||||||
|
|
@ -198,6 +213,8 @@ export default function AgAgentPanel({
|
||||||
<Send size={15} strokeWidth={1.75} />
|
<Send size={15} strokeWidth={1.75} />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -591,9 +591,14 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
||||||
.mail-label-pink { background:#fceeff; color:#8b5ca8; }
|
.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; }
|
.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 { 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-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: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-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-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 { 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 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); }
|
.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-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 { 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: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 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 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; }
|
.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-quick-btn-success:hover { background:#f3faf5; border-color:#c5e5d0; }
|
||||||
.agent-attention-avatar { width:30px!important; height:30px!important; font-size:10px!important; }
|
.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-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-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 { 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; }
|
.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-row:hover { background:#f5f4f8; }
|
||||||
.agent-agenda-bar { width:4px; height:100%; min-height:40px; border-radius:var(--radius-pill); background:#3d8a5d; }
|
.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-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.tone-amber .agent-agenda-bar { background:#b7892d; }
|
||||||
.agent-agenda-row time { display:block; color:#8d8792; font-size:10px; font-weight:600; }
|
.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; }
|
.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";
|
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) {
|
export function emailLabels(email) {
|
||||||
const text = emailSearchText(email);
|
const text = emailSearchText(email);
|
||||||
const labels = [];
|
const labels = [];
|
||||||
|
|
@ -48,7 +51,7 @@ export function emailLabels(email) {
|
||||||
if (/ai|agent|llm|gpt|model/.test(text)) {
|
if (/ai|agent|llm|gpt|model/.test(text)) {
|
||||||
labels.push({ name: "AI", tone: "pink" });
|
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" });
|
labels.push({ name: "Domains", tone: "amber" });
|
||||||
}
|
}
|
||||||
if (/linkedin|twitter|facebook|instagram|social/.test(text)) {
|
if (/linkedin|twitter|facebook|instagram|social/.test(text)) {
|
||||||
|
|
@ -81,19 +84,37 @@ export function categoryCounts(emails) {
|
||||||
return counts;
|
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) {
|
export function inboxIntelligence(emails) {
|
||||||
const unread = emails.filter((email) => email.is_read === false);
|
const unread = emails.filter((email) => email.is_read === false);
|
||||||
const important = emails.filter((email) =>
|
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) =>
|
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) =>
|
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) =>
|
const invoices = emails.filter((email) =>
|
||||||
/invoice|bill|payment|receipt/.test(emailSearchText(email)),
|
/invoice|bill|payment|receipt|purchase order|\bpo\b/.test(emailSearchText(email)),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
unread: unread.length,
|
unread: unread.length,
|
||||||
|
|
@ -172,7 +193,10 @@ export function inboxContext(emails) {
|
||||||
|
|
||||||
const needsReply = emails
|
const needsReply = emails
|
||||||
.filter((email) =>
|
.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),
|
emailSearchText(email),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -197,7 +221,13 @@ export function inboxContext(emails) {
|
||||||
if (!time) return;
|
if (!time) return;
|
||||||
const label = (email.subject || "Meeting").trim().slice(0, 42);
|
const label = (email.subject || "Meeting").trim().slice(0, 42);
|
||||||
const venue = extractVenue(text);
|
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)) {
|
if (!today.some((item) => item.time === time && item.label === label)) {
|
||||||
today.push({ time, label, venue, tone });
|
today.push({ time, label, venue, tone });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue