right side

This commit is contained in:
debu-2 2026-10-04 00:44:58 +05:30
parent 7fb9e0d5d4
commit f289d4a4d6
2 changed files with 195 additions and 104 deletions

View file

@ -1,7 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Calendar, Calendar,
Check,
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
FileSearch, FileSearch,
@ -18,12 +17,14 @@ import SenderAvatar from "./SenderAvatar";
import { inboxContext } from "../utils/inboxSignals"; import { inboxContext } from "../utils/inboxSignals";
const ICON = { size: 14, strokeWidth: 1.75 }; const ICON = { size: 14, strokeWidth: 1.75 };
const AGENDA_SLOTS = 3;
const ATTENTION_SLOTS = 2;
const cards = [ const cards = [
{ key: "important", label: "Important emails", captionKey: "importantFromClients", captionSuffix: "from clients", icon: Mail, tone: "rose" }, { 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", icon: ListChecks, tone: "blue" }, { 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", icon: Calendar, tone: "green" }, { 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", icon: Receipt, tone: "amber" }, { key: "invoices", label: "Invoices & bills", hint: "This week", emptyCaption: "No invoices detected", icon: Receipt, tone: "amber" },
]; ];
function greeting(name) { function greeting(name) {
@ -32,6 +33,70 @@ function greeting(name) {
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 (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>
);
}
export default function AgAgentPanel({ export default function AgAgentPanel({
account, account,
emails, emails,
@ -45,9 +110,8 @@ export default function AgAgentPanel({
const [agentQuery, setAgentQuery] = useState(""); const [agentQuery, setAgentQuery] = useState("");
const [panelOpen, setPanelOpen] = useState(true); const [panelOpen, setPanelOpen] = useState(true);
const firstName = account?.first_name || "there"; const firstName = account?.first_name || "there";
const activeCards = cards.filter((card) => (stats[card.key] || 0) > 0); const agendaSlots = padSlots(today, AGENDA_SLOTS);
const hasContext = needsReply.length > 0 || today.length > 0 || deadlines.length > 0; const attentionSlots = padSlots(needsReply, ATTENTION_SLOTS);
const hasSignals = activeCards.length > 0 || hasContext;
function submitAgentQuery(event) { function submitAgentQuery(event) {
event.preventDefault(); event.preventDefault();
@ -80,47 +144,39 @@ export default function AgAgentPanel({
{panelOpen && ( {panelOpen && (
<div className="agent-panel-body"> <div className="agent-panel-body">
{!insight && hasSignals && ( {!insight && (
<div className="agent-panel-hello"> <div className="agent-panel-hello">
<strong>{greeting(firstName)}</strong> <strong>{greeting(firstName)}</strong>
<span>Here&apos;s what needs your attention today.</span> <span>Here&apos;s what needs your attention today.</span>
</div> </div>
)} )}
{!insight && !hasSignals && ( {!insight && (
<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 && activeCards.length > 0 && (
<div className="agent-stat-grid"> <div className="agent-stat-grid">
{activeCards.map((card) => { {cards.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 isEmpty = count === 0;
? `${stats[card.captionKey]} ${card.captionSuffix}`
: 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}${isEmpty ? " agent-stat-empty" : ""}`}
onClick={() => onQuickAction(card.key)} onClick={() => onQuickAction(card.key)}
disabled={isEmpty}
aria-label={`${card.label}: ${count}`}
> >
<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>{statCaption(card, stats)}</small>
</button> </button>
); );
})} })}
</div> </div>
)} )}
{!insight && focus && hasSignals && <p className="agent-panel-note">{focus}</p>} {!insight && focus && <p className="agent-panel-note">{focus}</p>}
<div className="agent-quick"> <div className="agent-quick">
<h3>Quick actions</h3> <h3>Quick actions</h3>
@ -144,14 +200,17 @@ export default function AgAgentPanel({
</div> </div>
</div> </div>
{!insight && today.length > 0 && ( {!insight && (
<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>
<button type="button" className="agent-context-link">View calendar</button> <button type="button" className="agent-context-link">View calendar</button>
</div> </div>
<ul> <ul>
{today.map((item) => ( {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"}`}> <li key={`${item.time}-${item.label}`} className={`agent-agenda-row tone-${item.tone || "green"}`}>
<span className="agent-agenda-bar" aria-hidden="true" /> <span className="agent-agenda-bar" aria-hidden="true" />
<div> <div>
@ -161,19 +220,30 @@ export default function AgAgentPanel({
</div> </div>
<ChevronRight size={14} strokeWidth={1.75} /> <ChevronRight size={14} strokeWidth={1.75} />
</li> </li>
)
))} ))}
</ul> </ul>
</section> </section>
)} )}
{!insight && needsReply.length > 0 && ( {!insight && (
<section className="agent-context-block"> <section className="agent-context-block agent-attention-block">
<h3>Needs attention</h3> <h3>Needs attention</h3>
<ul className="agent-attention-list"> <ul className="agent-attention-list">
{needsReply.slice(0, 3).map((item) => ( {attentionSlots.map((item, index) => (
item.placeholder ? (
<AttentionPlaceholder
key={item.id}
index={index}
showEmptyMessage={needsReply.length === 0}
/>
) : (
<li key={item.id}> <li key={item.id}>
<button type="button" onClick={() => onOpenEmail?.(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" /> <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-copy">
<span className="agent-attention-name">{item.name}</span> <span className="agent-attention-name">{item.name}</span>
<span className="agent-attention-meta">{item.status}</span> <span className="agent-attention-meta">{item.status}</span>
@ -181,6 +251,7 @@ export default function AgAgentPanel({
<em>{item.age}</em> <em>{item.age}</em>
</button> </button>
</li> </li>
)
))} ))}
</ul> </ul>
</section> </section>

View file

@ -619,6 +619,10 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.agent-stat-blue { background:linear-gradient(180deg,#f3f7ff 0%,#eaf1ff 100%); border-color:#d5e2f8; color:#4d74c4; } .agent-stat-blue { background:linear-gradient(180deg,#f3f7ff 0%,#eaf1ff 100%); border-color:#d5e2f8; color:#4d74c4; }
.agent-stat-green { background:linear-gradient(180deg,#f1faf4 0%,#e8f5ed 100%); border-color:#cfe8d8; color:#3d8a5d; } .agent-stat-green { background:linear-gradient(180deg,#f1faf4 0%,#e8f5ed 100%); border-color:#cfe8d8; color:#3d8a5d; }
.agent-stat-amber { background:linear-gradient(180deg,#f7f2ff 0%,#efe8ff 100%); border-color:#ddd0f5; color:#8b6fd4; } .agent-stat-amber { background:linear-gradient(180deg,#f7f2ff 0%,#efe8ff 100%); border-color:#ddd0f5; color:#8b6fd4; }
.agent-stat-empty { opacity:.72; cursor:default; }
.agent-stat-empty:hover { transform:none; box-shadow:none; }
.agent-stat-empty strong { color:#b5b0ba; }
.agent-stat-empty small { color:#a39ea8; }
.agent-quick { margin-top:var(--space-1); padding-top:var(--space-3); border-top:1px solid var(--border-subtle); } .agent-quick { margin-top:var(--space-1); padding-top:var(--space-3); border-top:1px solid var(--border-subtle); }
.agent-quick h3 { margin:0 0 10px; color:#7a7580; font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; } .agent-quick h3 { margin:0 0 10px; color:#7a7580; font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; }
.agent-quick div { display:grid; grid-template-columns:1fr 1fr; gap:var(--space-2); } .agent-quick div { display:grid; grid-template-columns:1fr 1fr; gap:var(--space-2); }
@ -657,6 +661,22 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.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; }
.agent-agenda-row small { display:block; color:#8d8792; font-size:10px; } .agent-agenda-row small { display:block; color:#8d8792; font-size:10px; }
.agent-placeholder-row { border-style:dashed; background:#fafafa; pointer-events:none; }
.agent-placeholder-row .agent-agenda-bar.placeholder { background:#e4e0dc; min-height:36px; }
.agent-placeholder-row time,.agent-placeholder-row strong,.agent-placeholder-row small { color:#b8b3bc; }
.agent-placeholder-row strong { font-weight:500; }
.agent-placeholder-copy { min-width:0; display:grid; gap:5px; align-content:center; }
.agent-placeholder-line { display:block; height:8px; border-radius:var(--radius-pill); background:linear-gradient(90deg,#ece8e4 0%,#f4f1ee 100%); }
.agent-placeholder-line.wide { width:78%; }
.agent-placeholder-line.narrow { width:42%; }
.agent-attention-placeholder { padding:10px 0; display:grid; grid-template-columns:30px 1fr 24px; align-items:center; gap:var(--space-2); border-bottom:1px solid var(--border-subtle); pointer-events:none; }
.agent-attention-placeholder:last-child { border-bottom:0; }
.agent-placeholder-avatar { width:30px; height:30px; border-radius:50%; background:linear-gradient(180deg,#ece8e4 0%,#e2deda 100%); }
.agent-placeholder-avatar.muted { background:linear-gradient(180deg,#f0ece8 0%,#e8e4e0 100%); }
.agent-placeholder-label { color:#b8b3bc; font-size:var(--text-meta); font-weight:600; }
.agent-placeholder-copy small { color:#c5c0ba; font-size:10px; }
.agent-placeholder-age { width:20px; height:8px; border-radius:var(--radius-pill); background:#ece8e4; }
.agent-attention-block .agent-attention-list:has(.agent-attention-placeholder) .agent-attention-placeholder:first-child .agent-placeholder-line.wide { width:65%; }
.agent-attention-name { color:#2a2530; font-size:var(--text-meta); font-weight:600; } .agent-attention-name { color:#2a2530; font-size:var(--text-meta); font-weight:600; }
.agent-attention-meta { color:#8d8792; font-size:10px; } .agent-attention-meta { color:#8d8792; font-size:10px; }
.agent-attention-list button:hover .agent-attention-name { color:var(--coral); } .agent-attention-list button:hover .agent-attention-name { color:var(--coral); }