right side
This commit is contained in:
parent
7fb9e0d5d4
commit
f289d4a4d6
2 changed files with 195 additions and 104 deletions
|
|
@ -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's what needs your attention today.</span>
|
<span>Here'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'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's agenda</h3>
|
<h3>Today'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>
|
||||||
|
|
|
||||||
|
|
@ -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); }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue