370 lines
13 KiB
JavaScript
370 lines
13 KiB
JavaScript
import { useState } from "react";
|
|
import {
|
|
CalendarDays,
|
|
Check,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
ClipboardList,
|
|
CircleHelp,
|
|
FileSearch,
|
|
FileText,
|
|
ListChecks,
|
|
Mail,
|
|
Paperclip,
|
|
PenLine,
|
|
Send,
|
|
Settings,
|
|
Sparkles,
|
|
} from "lucide-react";
|
|
import AgentInsight from "./AgentInsight";
|
|
import AgentSpark from "./AgentSpark";
|
|
import SenderAvatar from "./SenderAvatar";
|
|
import { inboxContext } from "../utils/inboxSignals";
|
|
|
|
const ICON = { size: 14, strokeWidth: 1.75 };
|
|
const ICON_SM = { size: 13, strokeWidth: 1.75 };
|
|
|
|
const cards = [
|
|
{
|
|
key: "important",
|
|
label: "Important emails",
|
|
captionKey: "importantFromClients",
|
|
captionSuffix: "from clients",
|
|
hint: "In your inbox",
|
|
icon: Mail,
|
|
tone: "rose",
|
|
},
|
|
{
|
|
key: "actionItems",
|
|
label: "Action items",
|
|
hint: "Need your response",
|
|
icon: ClipboardList,
|
|
tone: "blue",
|
|
},
|
|
{
|
|
key: "meetings",
|
|
label: "Meetings mentioned",
|
|
hint: "In your emails",
|
|
icon: CalendarDays,
|
|
tone: "green",
|
|
},
|
|
{
|
|
key: "invoices",
|
|
label: "Invoices & bills",
|
|
hint: "This week",
|
|
icon: FileText,
|
|
tone: "purple",
|
|
},
|
|
];
|
|
|
|
function greeting(name) {
|
|
const hour = new Date().getHours();
|
|
const period = hour < 12 ? "Good morning" : hour < 17 ? "Good afternoon" : "Good evening";
|
|
return `${period}, ${name}`;
|
|
}
|
|
|
|
function statCaption(card, stats) {
|
|
const count = stats[card.key] || 0;
|
|
if (count === 0) return card.hint;
|
|
if (card.captionKey && stats[card.captionKey] > 0) {
|
|
return `${stats[card.captionKey]} ${card.captionSuffix}`;
|
|
}
|
|
return card.hint;
|
|
}
|
|
|
|
function metricTotal(stats) {
|
|
return cards.reduce((sum, card) => sum + (stats[card.key] || 0), 0);
|
|
}
|
|
|
|
export default function AgAgentPanel({
|
|
account,
|
|
emails,
|
|
focus,
|
|
insight,
|
|
loading = false,
|
|
mailboxEmpty = false,
|
|
isBrandNewMailbox = false,
|
|
onCompose,
|
|
onQuickAction,
|
|
onOpenEmail,
|
|
onAgentAsk,
|
|
}) {
|
|
const { stats, needsReply, today, deadlines } = inboxContext(emails);
|
|
const [agentQuery, setAgentQuery] = useState("");
|
|
const [panelOpen, setPanelOpen] = useState(true);
|
|
const firstName = account?.first_name || "there";
|
|
const allMetricsZero = metricTotal(stats) === 0;
|
|
const hasQuery = agentQuery.trim().length > 0;
|
|
const showOnboarding = !insight && !loading && mailboxEmpty && isBrandNewMailbox;
|
|
const showQuietEmpty = !insight && !loading && mailboxEmpty && !isBrandNewMailbox;
|
|
const showFullPanel = !insight && !loading && !mailboxEmpty;
|
|
|
|
function submitAgentQuery(event) {
|
|
event.preventDefault();
|
|
const query = agentQuery.trim();
|
|
if (!query) return;
|
|
onAgentAsk?.(query);
|
|
setAgentQuery("");
|
|
}
|
|
|
|
return (
|
|
<section className="agent-panel" aria-label="AG Agent">
|
|
<header className="agent-panel-head">
|
|
<span className="agent-panel-mark" aria-hidden="true">
|
|
<Sparkles {...ICON} />
|
|
</span>
|
|
<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={14} strokeWidth={1.75} className={panelOpen ? "open" : ""} />
|
|
</button>
|
|
</header>
|
|
|
|
<AgentInsight insight={insight} />
|
|
|
|
{panelOpen && (
|
|
<div className="agent-panel-body">
|
|
{!insight && (
|
|
<div className="agent-panel-hello">
|
|
<strong>{greeting(firstName)}</strong>
|
|
<span>
|
|
{showOnboarding
|
|
? "Your new inbox is ready."
|
|
: showQuietEmpty
|
|
? "Nothing needs your attention right now."
|
|
: "Here's what needs your attention today."}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{!insight && loading && (
|
|
<div className="agent-loading-block" aria-hidden="true">
|
|
<span className="agent-loading-pulse" />
|
|
<span className="agent-loading-pulse short" />
|
|
</div>
|
|
)}
|
|
|
|
{showOnboarding && (
|
|
<div className="agent-ready-card">
|
|
<p className="agent-ready-lead">
|
|
<Check size={14} strokeWidth={2} /> AGMail is ready
|
|
</p>
|
|
<p>
|
|
No messages yet. I'll start organizing and surfacing important
|
|
items as email arrives.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{showQuietEmpty && (
|
|
<div className="agent-caught-up">
|
|
<p className="agent-caught-up-lead">
|
|
<Check size={14} strokeWidth={2} /> You're all caught up.
|
|
</p>
|
|
<p>Your inbox is clear for now.</p>
|
|
</div>
|
|
)}
|
|
|
|
{showFullPanel && !allMetricsZero && (
|
|
<div className="agent-stat-grid">
|
|
{cards.map((card) => {
|
|
const Icon = card.icon;
|
|
const count = stats[card.key] || 0;
|
|
const isEmpty = count === 0;
|
|
return (
|
|
<button
|
|
key={card.key}
|
|
type="button"
|
|
className={`agent-stat agent-stat-${card.tone}${isEmpty ? " agent-stat-empty" : ""}`}
|
|
onClick={() => onQuickAction(card.key)}
|
|
disabled={isEmpty}
|
|
aria-label={`${card.label}: ${count}`}
|
|
>
|
|
<div className="agent-stat-top">
|
|
<span className="agent-stat-icon"><Icon {...ICON} /></span>
|
|
<strong>{count}</strong>
|
|
</div>
|
|
<span className="agent-stat-label">{card.label}</span>
|
|
<small>{statCaption(card, stats)}</small>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{showFullPanel && allMetricsZero && (
|
|
<div className="agent-caught-up">
|
|
<p className="agent-caught-up-lead">
|
|
<Check size={14} strokeWidth={2} /> You're all caught up.
|
|
</p>
|
|
<p>No urgent messages or outstanding actions.</p>
|
|
</div>
|
|
)}
|
|
|
|
{!insight && focus && showFullPanel && !allMetricsZero && (
|
|
<p className="agent-panel-note">{focus}</p>
|
|
)}
|
|
|
|
<section className="agent-quick">
|
|
<h3>{showOnboarding ? "Quick start" : "Quick actions"}</h3>
|
|
<div className="agent-quick-grid">
|
|
{showOnboarding ? (
|
|
<>
|
|
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("learn")}>
|
|
<AgentSpark className="agent-quick-icon agent-quick-icon-purple" />
|
|
<span>Meet AG Agent</span>
|
|
</button>
|
|
<button type="button" className="agent-quick-btn" onClick={() => onCompose?.()}>
|
|
<PenLine {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
|
<span>Compose email</span>
|
|
</button>
|
|
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("preferences")}>
|
|
<Settings {...ICON_SM} className="agent-quick-icon agent-quick-icon-muted" />
|
|
<span>Set preferences</span>
|
|
</button>
|
|
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("learn")}>
|
|
<CircleHelp {...ICON_SM} className="agent-quick-icon agent-quick-icon-slate" />
|
|
<span>Learn AGMail</span>
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className="agent-quick-btn"
|
|
onClick={() => onQuickAction("summarize")}
|
|
disabled={mailboxEmpty}
|
|
>
|
|
<Sparkles {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
|
<span>Summarize inbox</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="agent-quick-btn"
|
|
onClick={() => onQuickAction("draft")}
|
|
disabled={mailboxEmpty}
|
|
>
|
|
<PenLine {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
|
<span>Draft replies</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="agent-quick-btn"
|
|
onClick={() => onQuickAction("attachments")}
|
|
disabled={mailboxEmpty}
|
|
>
|
|
<FileSearch {...ICON_SM} className="agent-quick-icon agent-quick-icon-muted" />
|
|
<span>Find attachments</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="agent-quick-btn"
|
|
onClick={() => onQuickAction("actions")}
|
|
disabled={mailboxEmpty}
|
|
>
|
|
<ListChecks {...ICON_SM} className="agent-quick-icon agent-quick-icon-slate" />
|
|
<span>Show action items</span>
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{showFullPanel && !insight && (
|
|
<section className="agent-context-block agent-agenda">
|
|
<div className="agent-context-head">
|
|
<h3>Today's agenda</h3>
|
|
<button type="button" className="agent-context-link">View calendar</button>
|
|
</div>
|
|
{today.length > 0 ? (
|
|
<ul className="agent-agenda-list">
|
|
{today.map((item) => (
|
|
<li key={`${item.time}-${item.label}`} className="agent-agenda-row">
|
|
<span className={`agent-agenda-bar tone-${item.tone || "green"}`} aria-hidden="true" />
|
|
<time>{item.time}</time>
|
|
<div className="agent-agenda-copy">
|
|
<strong>{item.label}</strong>
|
|
{item.venue && <small>{item.venue}</small>}
|
|
</div>
|
|
<ChevronRight size={12} strokeWidth={1.75} className="agent-agenda-chevron" />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="agent-empty-line">No meetings in your emails today.</p>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{showFullPanel && !insight && (
|
|
<section className="agent-context-block agent-attention-block">
|
|
<h3>Needs attention</h3>
|
|
{needsReply.length > 0 ? (
|
|
<ul className="agent-attention-list">
|
|
{needsReply.slice(0, 3).map((item) => (
|
|
<li key={item.id}>
|
|
<button type="button" onClick={() => onOpenEmail?.(item.id)}>
|
|
<SenderAvatar
|
|
email={emails.find((entry) => entry.id === item.id) || { sender: { name: item.name } }}
|
|
className="agent-attention-avatar"
|
|
/>
|
|
<span className="agent-attention-copy">
|
|
<span className="agent-attention-name">{item.name}</span>
|
|
<span className="agent-attention-meta">{item.status}</span>
|
|
</span>
|
|
<em>{item.age}</em>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="agent-empty-line">Nothing needs your attention right now.</p>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{showFullPanel && !insight && deadlines.length > 0 && (
|
|
<section className="agent-context-block agent-deadlines">
|
|
<h3>Deadlines</h3>
|
|
<ul>
|
|
{deadlines.map((item) => (
|
|
<li key={`${item.label}-${item.due}`}>
|
|
<span>{item.label}</span>
|
|
<em>{item.due}</em>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
|
|
<form className="agent-ask-input" onSubmit={submitAgentQuery}>
|
|
<input
|
|
value={agentQuery}
|
|
onChange={(event) => setAgentQuery(event.target.value)}
|
|
placeholder="Ask about your emails..."
|
|
/>
|
|
<button type="button" className="agent-ask-attach" aria-label="Attach context" title="Attach context">
|
|
<Paperclip size={13} strokeWidth={1.75} />
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className={`agent-ask-send${hasQuery ? " active" : ""}`}
|
|
aria-label="Send to agent"
|
|
disabled={!hasQuery}
|
|
>
|
|
<Send size={13} strokeWidth={1.75} />
|
|
</button>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|