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 { 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,57 +61,68 @@ 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} />
{!insight && hasSignals && ( {panelOpen && (
<div className="agent-panel-hello"> <div className="agent-panel-body">
<strong>{greeting(firstName)}</strong> {!insight && hasSignals && (
<span>Here&apos;s what needs your attention today.</span> <div className="agent-panel-hello">
</div> <strong>{greeting(firstName)}</strong>
)} <span>Here&apos;s what needs your attention today.</span>
</div>
)}
{!insight && !hasSignals && ( {!insight && !hasSignals && (
<div className="agent-caught-up"> <div className="agent-caught-up">
<p className="agent-caught-up-lead"><Check size={15} strokeWidth={2} /> You&apos;re all caught up.</p> <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> <p>No urgent messages or outstanding actions.</p>
<small>Inbox is quiet right now.</small> <small>Inbox is quiet right now.</small>
</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]
? `${stats[card.captionKey]} ${card.captionSuffix}` ? `${stats[card.captionKey]} ${card.captionSuffix}`
: card.hint; : card.hint;
return ( return (
<button <button
key={card.key} key={card.key}
type="button" type="button"
className={`agent-stat agent-stat-${card.tone}`} className={`agent-stat agent-stat-${card.tone}`}
onClick={() => onQuickAction(card.key)} onClick={() => onQuickAction(card.key)}
> >
<span className="agent-stat-icon"><Icon {...ICON} /></span> <span className="agent-stat-icon"><Icon {...ICON} /></span>
<strong>{count}</strong> <strong>{count}</strong>
<span>{card.label}</span> <span>{card.label}</span>
<small>{caption}</small> <small>{caption}</small>
</button> </button>
); );
})} })}
</div> </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> <h3>Quick actions</h3>
<div> <div>
<button type="button" className="agent-quick-btn agent-quick-btn-agent" onClick={() => onQuickAction("summarize")}> <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 className="agent-quick-icon"><ListChecks {...ICON} /></span>
<span>Show action items</span> <span>Show action items</span>
</button> </button>
</div>
</div> </div>
</div>
{!insight && today.length > 0 && ( {!insight && today.length > 0 && (
<section className="agent-context-block agent-agenda"> <section className="agent-context-block agent-agenda">
<div className="agent-context-head"> <div className="agent-context-head">
<h3>Today&apos;s agenda</h3> <h3>Today&apos;s agenda</h3>
@ -148,10 +163,10 @@ export default function AgAgentPanel({
</li> </li>
))} ))}
</ul> </ul>
</section> </section>
)} )}
{!insight && needsReply.length > 0 && ( {!insight && needsReply.length > 0 && (
<section className="agent-context-block"> <section className="agent-context-block">
<h3>Needs attention</h3> <h3>Needs attention</h3>
<ul className="agent-attention-list"> <ul className="agent-attention-list">
@ -168,10 +183,10 @@ export default function AgAgentPanel({
</li> </li>
))} ))}
</ul> </ul>
</section> </section>
)} )}
{!insight && deadlines.length > 0 && ( {!insight && deadlines.length > 0 && (
<section className="agent-context-block agent-deadlines"> <section className="agent-context-block agent-deadlines">
<h3>Deadlines</h3> <h3>Deadlines</h3>
<ul> <ul>
@ -182,10 +197,10 @@ export default function AgAgentPanel({
</li> </li>
))} ))}
</ul> </ul>
</section> </section>
)} )}
<form className="agent-ask-input" onSubmit={submitAgentQuery}> <form className="agent-ask-input" onSubmit={submitAgentQuery}>
<input <input
value={agentQuery} value={agentQuery}
onChange={(event) => setAgentQuery(event.target.value)} onChange={(event) => setAgentQuery(event.target.value)}
@ -196,8 +211,10 @@ export default function AgAgentPanel({
</button> </button>
<button type="submit" className="agent-ask-send" aria-label="Send to agent" disabled={!agentQuery.trim()}> <button type="submit" className="agent-ask-send" aria-label="Send to agent" disabled={!agentQuery.trim()}>
<Send size={15} strokeWidth={1.75} /> <Send size={15} strokeWidth={1.75} />
</button> </button>
</form> </form>
</div>
)}
</section> </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; } .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; }

View file

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