right side

This commit is contained in:
debu-2 2026-10-04 00:41:19 +05:30
parent 5233481f06
commit 7fb9e0d5d4
3 changed files with 128 additions and 68 deletions

View file

@ -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&apos;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&apos;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&apos;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&apos;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&apos;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>
);
}

View file

@ -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; }

View file

@ -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 });
}