right side

This commit is contained in:
debu-2 2026-10-04 00:52:13 +05:30
parent f289d4a4d6
commit 192b810eb4
4 changed files with 375 additions and 216 deletions

View file

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

View file

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