right side
This commit is contained in:
parent
f289d4a4d6
commit
192b810eb4
4 changed files with 375 additions and 216 deletions
|
|
@ -1,100 +1,76 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Calendar,
|
||||
CalendarDays,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ClipboardList,
|
||||
FileSearch,
|
||||
FileText,
|
||||
ListChecks,
|
||||
Mail,
|
||||
Paperclip,
|
||||
PenLine,
|
||||
Receipt,
|
||||
Send,
|
||||
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 AGENDA_SLOTS = 3;
|
||||
const ATTENTION_SLOTS = 2;
|
||||
const ICON_SM = { size: 13, strokeWidth: 1.75 };
|
||||
|
||||
const cards = [
|
||||
{ key: "important", label: "Important emails", captionKey: "importantFromClients", captionSuffix: "from clients", emptyCaption: "No priority mail yet", icon: Mail, tone: "rose" },
|
||||
{ key: "actionItems", label: "Action items", hint: "Need your response", emptyCaption: "Nothing pending", icon: ListChecks, tone: "blue" },
|
||||
{ key: "meetings", label: "Meetings mentioned", hint: "In your emails", emptyCaption: "No meetings found", icon: Calendar, tone: "green" },
|
||||
{ key: "invoices", label: "Invoices & bills", hint: "This week", emptyCaption: "No invoices detected", icon: Receipt, tone: "amber" },
|
||||
{
|
||||
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}`;
|
||||
return `${period}, ${name}!`;
|
||||
}
|
||||
|
||||
function statCaption(card, stats) {
|
||||
const count = stats[card.key] || 0;
|
||||
if (count === 0) return card.emptyCaption || card.hint;
|
||||
if (count === 0) return card.hint;
|
||||
if (card.captionKey && stats[card.captionKey] > 0) {
|
||||
return `${stats[card.captionKey]} ${card.captionSuffix}`;
|
||||
}
|
||||
return card.hint;
|
||||
}
|
||||
|
||||
function padSlots(items, slots) {
|
||||
const result = items.slice(0, slots).map((item) => ({ ...item, placeholder: false }));
|
||||
while (result.length < slots) {
|
||||
result.push({ placeholder: true, id: `slot-${result.length}` });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function AgendaPlaceholder({ index }) {
|
||||
return (
|
||||
<li className="agent-agenda-row agent-placeholder-row" aria-hidden={index > 0}>
|
||||
<span className="agent-agenda-bar placeholder" aria-hidden="true" />
|
||||
<div className="agent-placeholder-copy">
|
||||
{index === 0 ? (
|
||||
<>
|
||||
<time>—</time>
|
||||
<strong>No meetings in your emails</strong>
|
||||
<small>Calendar events will appear here</small>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="agent-placeholder-line" />
|
||||
<span className="agent-placeholder-line wide" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function AttentionPlaceholder({ index, showEmptyMessage }) {
|
||||
return (
|
||||
<li className="agent-attention-placeholder" aria-hidden={index > 0 && !showEmptyMessage}>
|
||||
{showEmptyMessage && index === 0 ? (
|
||||
<>
|
||||
<span className="agent-placeholder-avatar muted" aria-hidden="true" />
|
||||
<span className="agent-placeholder-copy">
|
||||
<strong className="agent-placeholder-label">Nothing needs attention</strong>
|
||||
<small>Threads awaiting reply will appear here</small>
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="agent-placeholder-avatar" aria-hidden="true" />
|
||||
<span className="agent-placeholder-copy">
|
||||
<span className="agent-placeholder-line wide" />
|
||||
<span className="agent-placeholder-line narrow" />
|
||||
</span>
|
||||
<span className="agent-placeholder-age" aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
function metricTotal(stats) {
|
||||
return cards.reduce((sum, card) => sum + (stats[card.key] || 0), 0);
|
||||
}
|
||||
|
||||
export default function AgAgentPanel({
|
||||
|
|
@ -102,6 +78,7 @@ export default function AgAgentPanel({
|
|||
emails,
|
||||
focus,
|
||||
insight,
|
||||
loading = false,
|
||||
onQuickAction,
|
||||
onOpenEmail,
|
||||
onAgentAsk,
|
||||
|
|
@ -110,8 +87,8 @@ export default function AgAgentPanel({
|
|||
const [agentQuery, setAgentQuery] = useState("");
|
||||
const [panelOpen, setPanelOpen] = useState(true);
|
||||
const firstName = account?.first_name || "there";
|
||||
const agendaSlots = padSlots(today, AGENDA_SLOTS);
|
||||
const attentionSlots = padSlots(needsReply, ATTENTION_SLOTS);
|
||||
const allMetricsZero = metricTotal(stats) === 0;
|
||||
const hasQuery = agentQuery.trim().length > 0;
|
||||
|
||||
function submitAgentQuery(event) {
|
||||
event.preventDefault();
|
||||
|
|
@ -124,7 +101,9 @@ export default function AgAgentPanel({
|
|||
return (
|
||||
<section className="agent-panel" aria-label="AG Agent">
|
||||
<header className="agent-panel-head">
|
||||
<span className="agent-panel-mark" aria-hidden="true"><AgentSpark /></span>
|
||||
<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>
|
||||
|
|
@ -136,7 +115,7 @@ export default function AgAgentPanel({
|
|||
aria-label={panelOpen ? "Collapse agent panel" : "Expand agent panel"}
|
||||
onClick={() => setPanelOpen((open) => !open)}
|
||||
>
|
||||
<ChevronDown size={16} strokeWidth={1.75} className={panelOpen ? "open" : ""} />
|
||||
<ChevronDown size={14} strokeWidth={1.75} className={panelOpen ? "open" : ""} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
|
|
@ -151,7 +130,23 @@ export default function AgAgentPanel({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!insight && (
|
||||
{!insight && loading && (
|
||||
<div className="agent-loading-block" aria-hidden="true">
|
||||
<span className="agent-loading-pulse" />
|
||||
<span className="agent-loading-pulse short" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!insight && !loading && 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 && !loading && !allMetricsZero && (
|
||||
<div className="agent-stat-grid">
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
|
|
@ -166,9 +161,11 @@ export default function AgAgentPanel({
|
|||
disabled={isEmpty}
|
||||
aria-label={`${card.label}: ${count}`}
|
||||
>
|
||||
<span className="agent-stat-icon"><Icon {...ICON} /></span>
|
||||
<strong>{count}</strong>
|
||||
<span>{card.label}</span>
|
||||
<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>
|
||||
);
|
||||
|
|
@ -176,29 +173,29 @@ export default function AgAgentPanel({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!insight && focus && <p className="agent-panel-note">{focus}</p>}
|
||||
{!insight && focus && !allMetricsZero && <p className="agent-panel-note">{focus}</p>}
|
||||
|
||||
<div className="agent-quick">
|
||||
<section className="agent-quick">
|
||||
<h3>Quick actions</h3>
|
||||
<div>
|
||||
<button type="button" className="agent-quick-btn agent-quick-btn-agent" onClick={() => onQuickAction("summarize")}>
|
||||
<span className="agent-quick-icon"><AgentSpark /></span>
|
||||
<div className="agent-quick-grid">
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("summarize")}>
|
||||
<Sparkles {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
||||
<span>Summarize inbox</span>
|
||||
</button>
|
||||
<button type="button" className="agent-quick-btn agent-quick-btn-agent" onClick={() => onQuickAction("draft")}>
|
||||
<span className="agent-quick-icon"><PenLine {...ICON} /></span>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("draft")}>
|
||||
<PenLine {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
||||
<span>Draft replies</span>
|
||||
</button>
|
||||
<button type="button" className="agent-quick-btn agent-quick-btn-info" onClick={() => onQuickAction("attachments")}>
|
||||
<span className="agent-quick-icon"><FileSearch {...ICON} /></span>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("attachments")}>
|
||||
<FileSearch {...ICON_SM} className="agent-quick-icon agent-quick-icon-muted" />
|
||||
<span>Find attachments</span>
|
||||
</button>
|
||||
<button type="button" className="agent-quick-btn agent-quick-btn-success" onClick={() => onQuickAction("actions")}>
|
||||
<span className="agent-quick-icon"><ListChecks {...ICON} /></span>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("actions")}>
|
||||
<ListChecks {...ICON_SM} className="agent-quick-icon agent-quick-icon-slate" />
|
||||
<span>Show action items</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{!insight && (
|
||||
<section className="agent-context-block agent-agenda">
|
||||
|
|
@ -206,38 +203,41 @@ export default function AgAgentPanel({
|
|||
<h3>Today's agenda</h3>
|
||||
<button type="button" className="agent-context-link">View calendar</button>
|
||||
</div>
|
||||
<ul>
|
||||
{agendaSlots.map((item, index) => (
|
||||
item.placeholder ? (
|
||||
<AgendaPlaceholder key={item.id} index={index} />
|
||||
) : (
|
||||
<li key={`${item.time}-${item.label}`} className={`agent-agenda-row tone-${item.tone || "green"}`}>
|
||||
<span className="agent-agenda-bar" aria-hidden="true" />
|
||||
<div>
|
||||
<time>{item.time}</time>
|
||||
{loading ? (
|
||||
<div className="agent-loading-rows" aria-hidden="true">
|
||||
<span className="agent-loading-pulse" />
|
||||
<span className="agent-loading-pulse short" />
|
||||
</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={14} strokeWidth={1.75} />
|
||||
<ChevronRight size={12} strokeWidth={1.75} className="agent-agenda-chevron" />
|
||||
</li>
|
||||
)
|
||||
))}
|
||||
</ul>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="agent-empty-line">No meetings in your emails today.</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!insight && (
|
||||
<section className="agent-context-block agent-attention-block">
|
||||
<h3>Needs attention</h3>
|
||||
<ul className="agent-attention-list">
|
||||
{attentionSlots.map((item, index) => (
|
||||
item.placeholder ? (
|
||||
<AttentionPlaceholder
|
||||
key={item.id}
|
||||
index={index}
|
||||
showEmptyMessage={needsReply.length === 0}
|
||||
/>
|
||||
) : (
|
||||
{loading ? (
|
||||
<div className="agent-loading-rows" aria-hidden="true">
|
||||
<span className="agent-loading-pulse" />
|
||||
</div>
|
||||
) : 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
|
||||
|
|
@ -251,9 +251,11 @@ export default function AgAgentPanel({
|
|||
<em>{item.age}</em>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
))}
|
||||
</ul>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="agent-empty-line">Nothing needs your attention right now.</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
|
@ -278,10 +280,15 @@ export default function AgAgentPanel({
|
|||
placeholder="Ask about your emails..."
|
||||
/>
|
||||
<button type="button" className="agent-ask-attach" aria-label="Attach context" title="Attach context">
|
||||
<Paperclip size={15} strokeWidth={1.75} />
|
||||
<Paperclip size={13} strokeWidth={1.75} />
|
||||
</button>
|
||||
<button type="submit" className="agent-ask-send" aria-label="Send to agent" disabled={!agentQuery.trim()}>
|
||||
<Send size={15} strokeWidth={1.75} />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -701,6 +701,7 @@ export default function MailboxView({
|
|||
emails={listedEmails}
|
||||
focus={agentFocus}
|
||||
insight={insight}
|
||||
loading={status.loading}
|
||||
onQuickAction={runQuickAction}
|
||||
onOpenEmail={(emailId) => {
|
||||
const email = listedEmails.find((item) => item.id === emailId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue