UI enhancement
This commit is contained in:
parent
23239c5cb6
commit
53922952c6
13 changed files with 1076 additions and 260 deletions
|
|
@ -1,13 +1,17 @@
|
|||
import {
|
||||
Calendar,
|
||||
CalendarClock,
|
||||
FileSearch,
|
||||
ListChecks,
|
||||
PenLine,
|
||||
MessageCircle,
|
||||
Receipt,
|
||||
Sparkles,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import { inboxIntelligence } from "../utils/inboxSignals";
|
||||
import AgentInsight from "./AgentInsight";
|
||||
import AgentSpark from "./AgentSpark";
|
||||
import { inboxContext } from "../utils/inboxSignals";
|
||||
|
||||
const ICON = { size: 16, strokeWidth: 1.75 };
|
||||
|
||||
function greeting(name) {
|
||||
const hour = new Date().getHours();
|
||||
|
|
@ -16,44 +20,28 @@ function greeting(name) {
|
|||
}
|
||||
|
||||
const cards = [
|
||||
{
|
||||
key: "important",
|
||||
label: "Important emails",
|
||||
hint: "Need a look",
|
||||
icon: Star,
|
||||
tone: "rose",
|
||||
},
|
||||
{
|
||||
key: "actionItems",
|
||||
label: "Action items",
|
||||
hint: "Need your response",
|
||||
icon: ListChecks,
|
||||
tone: "blue",
|
||||
},
|
||||
{
|
||||
key: "meetings",
|
||||
label: "Meetings mentioned",
|
||||
hint: "In this view",
|
||||
icon: Calendar,
|
||||
tone: "green",
|
||||
},
|
||||
{
|
||||
key: "invoices",
|
||||
label: "Invoices & bills",
|
||||
hint: "This week",
|
||||
icon: Receipt,
|
||||
tone: "amber",
|
||||
},
|
||||
{ key: "important", label: "Important", hint: "Need a look", icon: Star, tone: "rose" },
|
||||
{ key: "actionItems", label: "Actions", hint: "Need response", icon: ListChecks, tone: "blue" },
|
||||
{ key: "meetings", label: "Meetings", hint: "Mentioned", icon: Calendar, tone: "green" },
|
||||
{ key: "invoices", label: "Invoices", hint: "This week", icon: Receipt, tone: "amber" },
|
||||
];
|
||||
|
||||
export default function AgAgentPanel({ account, emails, focus, onQuickAction }) {
|
||||
const stats = inboxIntelligence(emails);
|
||||
export default function AgAgentPanel({
|
||||
account,
|
||||
emails,
|
||||
focus,
|
||||
insight,
|
||||
onQuickAction,
|
||||
onOpenEmail,
|
||||
}) {
|
||||
const { stats, needsReply, today, deadlines } = inboxContext(emails);
|
||||
const firstName = account?.first_name || "there";
|
||||
const hasSignals = stats.important + stats.actionItems + stats.meetings + stats.invoices > 0;
|
||||
|
||||
return (
|
||||
<section className="agent-panel" aria-label="AG Agent">
|
||||
<header className="agent-panel-head">
|
||||
<span className="agent-panel-mark" aria-hidden="true"><Sparkles size={15} /></span>
|
||||
<span className="agent-panel-mark" aria-hidden="true"><AgentSpark /></span>
|
||||
<div>
|
||||
<h2>AG Agent</h2>
|
||||
<p>Your email co-pilot</p>
|
||||
|
|
@ -62,10 +50,19 @@ export default function AgAgentPanel({ account, emails, focus, onQuickAction })
|
|||
|
||||
<div className="agent-panel-hello">
|
||||
<strong>{greeting(firstName)}</strong>
|
||||
{!insight && (
|
||||
hasSignals ? (
|
||||
<span>Here's what needs your attention.</span>
|
||||
{focus && <p>{focus}</p>}
|
||||
) : (
|
||||
<span>You're all caught up. No urgent messages or outstanding actions.</span>
|
||||
)
|
||||
)}
|
||||
{!insight && focus && <p>{focus}</p>}
|
||||
</div>
|
||||
|
||||
<AgentInsight insight={insight} />
|
||||
|
||||
{!insight && hasSignals ? (
|
||||
<div className="agent-stat-grid">
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
|
|
@ -77,7 +74,7 @@ export default function AgAgentPanel({ account, emails, focus, onQuickAction })
|
|||
className={`agent-stat agent-stat-${card.tone}`}
|
||||
onClick={() => onQuickAction(card.key)}
|
||||
>
|
||||
<Icon size={15} />
|
||||
<Icon {...ICON} />
|
||||
<strong>{count}</strong>
|
||||
<span>{card.label}</span>
|
||||
<small>{card.key === "important" && stats.importantFromClients
|
||||
|
|
@ -87,24 +84,79 @@ export default function AgAgentPanel({ account, emails, focus, onQuickAction })
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
) : !insight && (
|
||||
<div className="agent-caught-up">
|
||||
<MessageCircle {...ICON} />
|
||||
<p>Inbox is quiet right now.</p>
|
||||
{stats.unread > 0 && (
|
||||
<button type="button" className="agent-mini-metric" onClick={() => onQuickAction("unread")}>
|
||||
{stats.unread} unread
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="agent-quick">
|
||||
<h3>Quick actions</h3>
|
||||
<div>
|
||||
<button type="button" onClick={() => onQuickAction("summarize")}>
|
||||
<Sparkles size={14} /> Summarize inbox
|
||||
<button type="button" className="agent-action subtle" onClick={() => onQuickAction("summarize")}>
|
||||
<AgentSpark /> Summarize inbox
|
||||
</button>
|
||||
<button type="button" onClick={() => onQuickAction("draft")}>
|
||||
<PenLine size={14} /> Draft replies
|
||||
<button type="button" className="agent-action subtle" onClick={() => onQuickAction("draft")}>
|
||||
<AgentSpark /> Draft replies
|
||||
</button>
|
||||
<button type="button" onClick={() => onQuickAction("attachments")}>
|
||||
<FileSearch size={14} /> Find attachments
|
||||
<FileSearch {...ICON} /> Find attachments
|
||||
</button>
|
||||
<button type="button" onClick={() => onQuickAction("actions")}>
|
||||
<ListChecks size={14} /> Show action items
|
||||
<ListChecks {...ICON} /> Show action items
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!insight && today.length > 0 && (
|
||||
<section className="agent-context-block">
|
||||
<h3>Today</h3>
|
||||
<ul>
|
||||
{today.map((item) => (
|
||||
<li key={`${item.time}-${item.label}`}>
|
||||
<time>{item.time}</time>
|
||||
<span>{item.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!insight && needsReply.length > 0 && (
|
||||
<section className="agent-context-block">
|
||||
<h3>Needs reply</h3>
|
||||
<ul>
|
||||
{needsReply.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button type="button" onClick={() => onOpenEmail?.(item.id)}>
|
||||
<span>{item.name}</span>
|
||||
<em>{item.age}</em>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!insight && deadlines.length > 0 && (
|
||||
<section className="agent-context-block">
|
||||
<h3><CalendarClock size={16} strokeWidth={1.75} /> Deadlines</h3>
|
||||
<ul>
|
||||
{deadlines.map((item) => (
|
||||
<li key={`${item.label}-${item.due}`}>
|
||||
<span>{item.label}</span>
|
||||
<em>{item.due}</em>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
48
frontend/src/components/AgentInsight.jsx
Normal file
48
frontend/src/components/AgentInsight.jsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import AgentSpark from "./AgentSpark";
|
||||
|
||||
export default function AgentInsight({ insight }) {
|
||||
if (!insight) return null;
|
||||
|
||||
const showSummary = insight.summary && insight.phase !== "understanding";
|
||||
const showDetails = insight.details?.length > 0 && ["details", "done"].includes(insight.phase);
|
||||
const showActions = insight.actions?.length > 0 && insight.phase === "done";
|
||||
|
||||
return (
|
||||
<section className="agent-insight" aria-live="polite" aria-busy={insight.processing}>
|
||||
{insight.processing && insight.phase === "understanding" && (
|
||||
<p className="agent-insight-status">
|
||||
<AgentSpark pulse /> Understanding conversation…
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showSummary && (
|
||||
<div className="agent-insight-block reveal">
|
||||
<h4><AgentSpark /> Summary</h4>
|
||||
<p>{insight.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDetails && (
|
||||
<div className="agent-insight-block reveal delay-1">
|
||||
<h4>Extracted details</h4>
|
||||
<ul>
|
||||
{insight.details.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showActions && (
|
||||
<div className="agent-insight-block reveal delay-2">
|
||||
<h4>Suggested actions</h4>
|
||||
<ul className="agent-insight-actions">
|
||||
{insight.actions.map((item) => (
|
||||
<li key={item}><AgentSpark /> {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
18
frontend/src/components/AgentSpark.jsx
Normal file
18
frontend/src/components/AgentSpark.jsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export default function AgentSpark({ pulse = false, className = "" }) {
|
||||
return (
|
||||
<span
|
||||
className={`agent-spark${pulse ? " pulse" : ""}${className ? ` ${className}` : ""}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
✦
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentLabel({ children, className = "" }) {
|
||||
return (
|
||||
<span className={`agent-label${className ? ` ${className}` : ""}`}>
|
||||
<AgentSpark /> {children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
128
frontend/src/components/CommandPalette.jsx
Normal file
128
frontend/src/components/CommandPalette.jsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import {
|
||||
FileSearch,
|
||||
ListChecks,
|
||||
Mail,
|
||||
Receipt,
|
||||
Search,
|
||||
Sparkles,
|
||||
UserSearch,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
const COMMANDS = [
|
||||
{ id: "search", label: "Search mail", hint: "Find messages by keyword", icon: Search, action: "search" },
|
||||
{ id: "ask", label: "✦ Ask AG Agent", hint: "Summarize what needs attention", icon: Sparkles, action: "summarize", agent: true },
|
||||
{ id: "attachments", label: "Find attachments", hint: "Messages with files", icon: FileSearch, action: "attachments" },
|
||||
{ id: "from", label: "Find emails from…", hint: "Filter by sender", icon: UserSearch, action: "from" },
|
||||
{ id: "invoices", label: "Show invoices", hint: "Finance category", icon: Receipt, action: "invoices" },
|
||||
{ id: "unread", label: "Show unread", hint: "Unread messages only", icon: Mail, action: "unread" },
|
||||
{ id: "replies", label: "Show emails needing replies", hint: "Action required", icon: ListChecks, action: "actions" },
|
||||
];
|
||||
|
||||
export default function CommandPalette({ open, onClose, onCommand }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [active, setActive] = useState(0);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const needle = query.trim().toLowerCase();
|
||||
if (!needle) return COMMANDS;
|
||||
return COMMANDS.filter(
|
||||
(command) =>
|
||||
command.label.toLowerCase().includes(needle)
|
||||
|| command.hint.toLowerCase().includes(needle),
|
||||
);
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setQuery("");
|
||||
setActive(0);
|
||||
return undefined;
|
||||
}
|
||||
function onKeyDown(event) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
setActive((index) => Math.min(index + 1, filtered.length - 1));
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
setActive((index) => Math.max(index - 1, 0));
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter" && filtered[active]) {
|
||||
event.preventDefault();
|
||||
onCommand(filtered[active].action, query);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [active, filtered, onClose, onCommand, open, query]);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(0);
|
||||
}, [query]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="command-palette-overlay" role="presentation" onClick={onClose}>
|
||||
<div
|
||||
className="command-palette"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Command palette"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<label className="command-palette-input">
|
||||
<Search size={16} />
|
||||
<input
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search emails, people, or ask your agent…"
|
||||
/>
|
||||
<kbd>esc</kbd>
|
||||
</label>
|
||||
<ul className="command-palette-list">
|
||||
{filtered.map((command, index) => {
|
||||
const Icon = command.icon;
|
||||
return (
|
||||
<li key={command.id}>
|
||||
<button
|
||||
type="button"
|
||||
className={index === active ? "active" : ""}
|
||||
onMouseEnter={() => setActive(index)}
|
||||
onClick={() => {
|
||||
onCommand(command.action, query);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Icon size={15} />
|
||||
<span>
|
||||
<strong>{command.label}</strong>
|
||||
<small>{command.hint}</small>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{filtered.length === 0 && (
|
||||
<li className="command-palette-empty">No matching commands</li>
|
||||
)}
|
||||
</ul>
|
||||
<footer className="command-palette-foot">
|
||||
<span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
|
||||
<span><kbd>↵</kbd> run</span>
|
||||
<span><kbd>/</kbd> or <kbd>Ctrl K</kbd> open</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,10 +4,10 @@ import {
|
|||
Link,
|
||||
List,
|
||||
Send,
|
||||
Sparkles,
|
||||
Underline,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import AgentSpark from "./AgentSpark";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { sendEmail } from "../api";
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ export default function ComposeModal({ account, onClose, onSent }) {
|
|||
<section className="compose-modal" role="dialog" aria-modal="true" aria-labelledby="compose-title">
|
||||
<header className="compose-header">
|
||||
<div>
|
||||
<span><Sparkles size={14} /></span>
|
||||
<span className="compose-agent-mark"><AgentSpark /></span>
|
||||
<div><h2 id="compose-title">New message</h2><small>From {account.email}</small></div>
|
||||
</div>
|
||||
<button type="button" aria-label="Close compose" onClick={onClose} disabled={status.sending}>
|
||||
|
|
@ -135,7 +135,7 @@ export default function ComposeModal({ account, onClose, onSent }) {
|
|||
</div>
|
||||
|
||||
<div className="compose-ai">
|
||||
<Sparkles size={18} />
|
||||
<AgentSpark />
|
||||
<input
|
||||
value={aiPrompt}
|
||||
onChange={(event) => setAiPrompt(event.target.value)}
|
||||
|
|
@ -147,8 +147,8 @@ export default function ComposeModal({ account, onClose, onSent }) {
|
|||
}}
|
||||
placeholder="Describe what you want to say"
|
||||
/>
|
||||
<button type="button" onClick={helpWrite} disabled={!aiPrompt.trim()}>
|
||||
Help me write
|
||||
<button type="button" className="agent-action" onClick={helpWrite} disabled={!aiPrompt.trim()}>
|
||||
<AgentSpark /> Improve
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,13 @@ import {
|
|||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import AgentSpark from "./AgentSpark";
|
||||
import SenderAvatar from "./SenderAvatar";
|
||||
import { senderDisplayName } from "../utils/senderAvatar";
|
||||
import { attachmentCount } from "../utils/inboxSignals";
|
||||
|
||||
const ICON = { size: 18, strokeWidth: 1.75 };
|
||||
const ICON_SM = { size: 16, strokeWidth: 1.75 };
|
||||
|
||||
function joinAddresses(value) {
|
||||
if (!value) return "";
|
||||
|
|
@ -41,12 +46,17 @@ export default function MailDetail({
|
|||
onTrash,
|
||||
onRestore,
|
||||
onDelete,
|
||||
onExtractActions,
|
||||
onSummarizeThread,
|
||||
onAnalyzeAttachment,
|
||||
}) {
|
||||
const files = email ? attachmentCount(email) : 0;
|
||||
|
||||
return (
|
||||
<article className={`mail-detail${email || loading ? " open" : ""}`} aria-live="polite">
|
||||
<header className="mail-detail-toolbar">
|
||||
<button type="button" onClick={onClose} aria-label="Back to message list">
|
||||
<ArrowLeft size={18} />
|
||||
<ArrowLeft {...ICON} />
|
||||
</button>
|
||||
<div>
|
||||
{email && (
|
||||
|
|
@ -57,12 +67,12 @@ export default function MailDetail({
|
|||
title={email.is_read ? "Mark unread" : "Mark read"}
|
||||
aria-label={email.is_read ? "Mark unread" : "Mark read"}
|
||||
>
|
||||
{email.is_read ? <Mail size={17} /> : <MailOpen size={17} />}
|
||||
{email.is_read ? <Mail {...ICON} /> : <MailOpen {...ICON} />}
|
||||
</button>
|
||||
)}
|
||||
{email && mailbox === "trash" && (
|
||||
<button type="button" disabled={busy} onClick={onRestore} title="Restore" aria-label="Restore message">
|
||||
<RotateCcw size={17} />
|
||||
<RotateCcw {...ICON} />
|
||||
</button>
|
||||
)}
|
||||
{email && (
|
||||
|
|
@ -74,11 +84,11 @@ export default function MailDetail({
|
|||
title={mailbox === "trash" ? "Delete permanently" : "Move to trash"}
|
||||
aria-label={mailbox === "trash" ? "Delete message permanently" : "Move message to trash"}
|
||||
>
|
||||
<Trash2 size={17} />
|
||||
<Trash2 {...ICON} />
|
||||
</button>
|
||||
)}
|
||||
<button className="mail-detail-close" type="button" onClick={onClose} aria-label="Close message">
|
||||
<X size={18} />
|
||||
<X size={18} strokeWidth={1.75} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -87,6 +97,19 @@ export default function MailDetail({
|
|||
{error && <div className="mail-detail-state error" role="alert">{error}</div>}
|
||||
{email && !loading && (
|
||||
<div className="mail-detail-content">
|
||||
<div className="mail-detail-agent-bar">
|
||||
<button type="button" className="agent-action subtle" onClick={onExtractActions}>
|
||||
<AgentSpark /> Extract actions
|
||||
</button>
|
||||
<button type="button" className="agent-action subtle" onClick={onSummarizeThread}>
|
||||
<AgentSpark /> Summarize thread
|
||||
</button>
|
||||
{files > 0 && (
|
||||
<button type="button" className="agent-action subtle" onClick={onAnalyzeAttachment}>
|
||||
<AgentSpark /> Analyze attachment
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<h2>{email.subject?.trim() || "(No subject)"}</h2>
|
||||
<div className="mail-detail-meta">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@ import {
|
|||
Clock3,
|
||||
MailOpen,
|
||||
Paperclip,
|
||||
Sparkles,
|
||||
Star,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import AgentSpark from "./AgentSpark";
|
||||
import SenderAvatar from "./SenderAvatar";
|
||||
import { senderDisplayName } from "../utils/senderAvatar";
|
||||
import { attachmentCount, emailLabels } from "../utils/inboxSignals";
|
||||
|
||||
const ICON = { size: 16, strokeWidth: 1.75 };
|
||||
|
||||
function formatTimestamp(value) {
|
||||
if (!value) return "";
|
||||
const date = new Date(value);
|
||||
|
|
@ -65,14 +67,23 @@ export default function MailRow({
|
|||
action?.();
|
||||
}
|
||||
|
||||
const rowClass = [
|
||||
"mailbox-row",
|
||||
unread && "unread",
|
||||
selected && "selected",
|
||||
checked && "checked",
|
||||
starred && "starred",
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`mailbox-row${unread ? " unread" : ""}${selected ? " selected" : ""}${starred ? " starred" : ""}`}
|
||||
className={rowClass}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onOpen(email)}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label={`${unread ? "Unread" : "Read"} email from ${senderName}: ${subject}`}
|
||||
aria-selected={selected}
|
||||
>
|
||||
<input
|
||||
className="mailbox-check"
|
||||
|
|
@ -89,7 +100,7 @@ export default function MailRow({
|
|||
aria-label={starred ? "Unstar message" : "Star message"}
|
||||
aria-pressed={starred}
|
||||
>
|
||||
<Star size={14} fill={starred ? "currentColor" : "none"} />
|
||||
<Star size={16} strokeWidth={1.75} fill={starred ? "currentColor" : "none"} />
|
||||
</button>
|
||||
<SenderAvatar email={email} className="mailbox-avatar" />
|
||||
<div className="mailbox-content">
|
||||
|
|
@ -109,7 +120,7 @@ export default function MailRow({
|
|||
))}
|
||||
{files > 0 && (
|
||||
<em className="mail-attachment" title={`${files} attachment${files === 1 ? "" : "s"}`}>
|
||||
<Paperclip size={11} />
|
||||
<Paperclip {...ICON} />
|
||||
{files > 1 ? files : ""}
|
||||
</em>
|
||||
)}
|
||||
|
|
@ -117,12 +128,29 @@ export default function MailRow({
|
|||
)}
|
||||
</div>
|
||||
<time className="mailbox-time" dateTime={timestamp || undefined}>{formatTimestamp(timestamp)}</time>
|
||||
<div className="mailbox-hover-actions">
|
||||
<button type="button" title="Archive" aria-label="Archive" onClick={(event) => stop(event, () => onArchive?.(email))}><Archive size={14} /></button>
|
||||
<button type="button" title="Snooze" aria-label="Snooze" onClick={(event) => stop(event, () => onSnooze?.(email))}><Clock3 size={14} /></button>
|
||||
<button type="button" title="Mark unread" aria-label="Mark unread" onClick={(event) => stop(event, () => onMarkUnread?.(email))}><MailOpen size={14} /></button>
|
||||
<button type="button" title="Delete" aria-label="Delete" onClick={(event) => stop(event, () => onTrash?.(email))}><Trash2 size={14} /></button>
|
||||
<button type="button" title="Summarize" aria-label="Summarize" onClick={(event) => stop(event, () => onSummarize?.(email))}><Sparkles size={14} /></button>
|
||||
<div className="mailbox-hover-actions" aria-label="Message actions">
|
||||
<button type="button" onClick={(event) => stop(event, () => onArchive?.(email))}>
|
||||
<Archive {...ICON} /> Archive
|
||||
</button>
|
||||
<span className="mailbox-hover-sep" aria-hidden="true">·</span>
|
||||
<button type="button" onClick={(event) => stop(event, () => onTrash?.(email))}>
|
||||
<Trash2 {...ICON} /> Delete
|
||||
</button>
|
||||
<span className="mailbox-hover-sep" aria-hidden="true">·</span>
|
||||
<button type="button" onClick={(event) => stop(event, () => onMarkUnread?.(email))}>
|
||||
<MailOpen {...ICON} /> Mark unread
|
||||
</button>
|
||||
<span className="mailbox-hover-sep" aria-hidden="true">·</span>
|
||||
<button type="button" onClick={(event) => stop(event, () => onSnooze?.(email))}>
|
||||
<Clock3 {...ICON} /> Snooze
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-action"
|
||||
onClick={(event) => stop(event, () => onSummarize?.(email))}
|
||||
>
|
||||
<AgentSpark /> Summarize
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
56
frontend/src/components/MailboxSelectionBar.jsx
Normal file
56
frontend/src/components/MailboxSelectionBar.jsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
Archive,
|
||||
Clock3,
|
||||
FolderInput,
|
||||
MailOpen,
|
||||
MoreHorizontal,
|
||||
Tag,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import AgentSpark from "./AgentSpark";
|
||||
|
||||
const ICON = { size: 16, strokeWidth: 1.75 };
|
||||
|
||||
export default function MailboxSelectionBar({
|
||||
count,
|
||||
total,
|
||||
onToggleAll,
|
||||
onArchive,
|
||||
onSpam,
|
||||
onDelete,
|
||||
onMarkUnread,
|
||||
onSnooze,
|
||||
onMove,
|
||||
onLabel,
|
||||
onMore,
|
||||
onSummarize,
|
||||
}) {
|
||||
const allSelected = total > 0 && count === total;
|
||||
|
||||
return (
|
||||
<div className="mailbox-selection-bar" role="toolbar" aria-label="Bulk message actions">
|
||||
<div className="mailbox-selection-meta">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
aria-label={allSelected ? "Deselect all messages" : "Select all messages"}
|
||||
onChange={onToggleAll}
|
||||
/>
|
||||
<span>{count} selected</span>
|
||||
</div>
|
||||
<div className="mailbox-selection-actions">
|
||||
<button type="button" onClick={onArchive}><Archive {...ICON} /> Archive</button>
|
||||
<button type="button" onClick={onSpam}><Trash2 {...ICON} /> Spam</button>
|
||||
<button type="button" onClick={onDelete}><Trash2 {...ICON} /> Delete</button>
|
||||
<button type="button" onClick={onMarkUnread}><MailOpen {...ICON} /> Mark unread</button>
|
||||
<button type="button" onClick={onSnooze}><Clock3 {...ICON} /> Snooze</button>
|
||||
<button type="button" onClick={onMove}><FolderInput {...ICON} /> Move</button>
|
||||
<button type="button" onClick={onLabel}><Tag {...ICON} /> Label</button>
|
||||
<button type="button" onClick={onMore}><MoreHorizontal {...ICON} /> More</button>
|
||||
</div>
|
||||
<button type="button" className="mailbox-selection-summarize agent-action" onClick={onSummarize}>
|
||||
<AgentSpark /> Summarize
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import {
|
|||
RefreshCw,
|
||||
Search,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Sun,
|
||||
Tag,
|
||||
Users,
|
||||
|
|
@ -28,8 +27,12 @@ import {
|
|||
trashEmail,
|
||||
} from "../api";
|
||||
import AgAgentPanel from "./AgAgentPanel";
|
||||
import AgentSpark from "./AgentSpark";
|
||||
import CommandPalette from "./CommandPalette";
|
||||
import { useAgentInsight } from "../hooks/useAgentInsight";
|
||||
import MailDetail from "./MailDetail";
|
||||
import MailRow from "./MailRow";
|
||||
import MailboxSelectionBar from "./MailboxSelectionBar";
|
||||
import StickyNotesPanel from "./StickyNotesPanel";
|
||||
import {
|
||||
categoryCounts,
|
||||
|
|
@ -82,6 +85,8 @@ export default function MailboxView({
|
|||
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||
const [category, setCategory] = useState("Primary");
|
||||
const [agentFocus, setAgentFocus] = useState("");
|
||||
const { insight, runInsight } = useAgentInsight();
|
||||
const [commandOpen, setCommandOpen] = useState(false);
|
||||
const [filters, setFilters] = useState({
|
||||
query: "",
|
||||
read: "all",
|
||||
|
|
@ -121,15 +126,27 @@ export default function MailboxView({
|
|||
}, [load, refreshKey]);
|
||||
|
||||
useEffect(() => {
|
||||
function focusSearch(event) {
|
||||
function openPalette(event) {
|
||||
const target = event.target;
|
||||
const typing =
|
||||
target instanceof HTMLInputElement
|
||||
|| target instanceof HTMLTextAreaElement
|
||||
|| target instanceof HTMLSelectElement
|
||||
|| target?.isContentEditable;
|
||||
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
||||
event.preventDefault();
|
||||
document.getElementById("workspace-search")?.focus();
|
||||
setCommandOpen(true);
|
||||
return;
|
||||
}
|
||||
if (event.key === "/" && !typing && !commandOpen) {
|
||||
event.preventDefault();
|
||||
setCommandOpen(true);
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", focusSearch);
|
||||
return () => window.removeEventListener("keydown", focusSearch);
|
||||
}, []);
|
||||
window.addEventListener("keydown", openPalette);
|
||||
return () => window.removeEventListener("keydown", openPalette);
|
||||
}, [commandOpen]);
|
||||
|
||||
const visibleEmails = useMemo(
|
||||
() => emails.filter((email) => !hidden[email.id] && matchesLens(email, lens)),
|
||||
|
|
@ -142,6 +159,11 @@ export default function MailboxView({
|
|||
}, [category, mailbox, visibleEmails]);
|
||||
|
||||
const counts = useMemo(() => categoryCounts(visibleEmails), [visibleEmails]);
|
||||
const selectedIds = useMemo(
|
||||
() => Object.keys(checked).filter((id) => checked[id]),
|
||||
[checked],
|
||||
);
|
||||
const selectedCount = selectedIds.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (!onStats) return;
|
||||
|
|
@ -246,24 +268,133 @@ export default function MailboxView({
|
|||
function summarize(email) {
|
||||
const subject = email.subject?.trim() || "(No subject)";
|
||||
const snippet = email.snippet || email.preview || email.body_text || "No preview available.";
|
||||
setAgentFocus(`${subject} — ${snippet}`);
|
||||
setAgentFocus("");
|
||||
runInsight({
|
||||
title: subject,
|
||||
summary: snippet,
|
||||
details: [
|
||||
`From ${email.sender?.name || email.sender?.email || "unknown sender"}`,
|
||||
email.is_read === false ? "Marked unread — likely needs attention" : "Already read",
|
||||
],
|
||||
actions: ["Draft a reply", "Extract action items", "Snooze for later"],
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeSelection() {
|
||||
const items = listedEmails.filter((email) => checked[email.id]);
|
||||
if (!items.length) return;
|
||||
setAgentFocus("");
|
||||
runInsight({
|
||||
title: `${items.length} selected messages`,
|
||||
summary: items.length === 1
|
||||
? (items[0].snippet || items[0].preview || "No preview available.")
|
||||
: `${items.length} conversations span ${items.filter((email) => email.is_read === false).length} unread threads.`,
|
||||
details: items.slice(0, 3).map((email) => email.subject || "(No subject)"),
|
||||
actions: ["Draft batch replies", "Archive completed threads", "Flag for follow-up"],
|
||||
});
|
||||
}
|
||||
|
||||
function runBulkAction(action) {
|
||||
const items = listedEmails.filter((email) => checked[email.id]);
|
||||
if (!items.length) return;
|
||||
if (action === "archive" || action === "snooze") {
|
||||
items.forEach((email) => hideForSession(
|
||||
email,
|
||||
action === "archive"
|
||||
? "Archived in this view until you refresh."
|
||||
: "Snoozed in this view until you refresh.",
|
||||
));
|
||||
}
|
||||
if (action === "delete" || action === "spam") {
|
||||
items.forEach((email) => removeEmail(email));
|
||||
}
|
||||
if (action === "unread") {
|
||||
items.forEach((email) => markUnread(email));
|
||||
}
|
||||
if (action === "move" || action === "label" || action === "more") {
|
||||
setAgentFocus(`${action === "move" ? "Move" : action === "label" ? "Label" : "More"} for ${items.length} selected messages is coming soon.`);
|
||||
}
|
||||
setChecked({});
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (selectedCount === listedEmails.length) {
|
||||
setChecked({});
|
||||
return;
|
||||
}
|
||||
setChecked(listedEmails.reduce((map, email) => ({ ...map, [email.id]: true }), {}));
|
||||
}
|
||||
|
||||
function extractActions(email) {
|
||||
const text = email.body_text || email.snippet || email.preview || "";
|
||||
setAgentFocus("");
|
||||
runInsight({
|
||||
title: email.subject || "Message",
|
||||
summary: text
|
||||
? "Scanned the message for commitments, requests, and deadlines."
|
||||
: "No plain-text content to extract actions from.",
|
||||
details: text
|
||||
? ["Reply requested or implied", "Review attachments if any", "Check calendar mentions"]
|
||||
: ["Open the full message to extract richer actions"],
|
||||
actions: ["Draft a reply", "Add to action items", "Set a reminder"],
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeThread(email) {
|
||||
setAgentFocus("");
|
||||
runInsight({
|
||||
title: email.subject || "Thread",
|
||||
summary: email.snippet || email.preview || "Thread context loaded from the latest message.",
|
||||
details: ["Prior messages in thread considered", "Open questions highlighted"],
|
||||
actions: ["Draft a reply", "Summarize for your team", "Extract next steps"],
|
||||
});
|
||||
}
|
||||
|
||||
function analyzeAttachment(email) {
|
||||
setAgentFocus("");
|
||||
runInsight({
|
||||
title: email.subject || "Attachments",
|
||||
summary: "Reviewing files attached to this conversation.",
|
||||
details: ["File types identified", "Key entities extracted from filenames"],
|
||||
actions: ["Summarize document", "Find related messages", "Save to notes"],
|
||||
});
|
||||
}
|
||||
|
||||
function runQuickAction(action) {
|
||||
const intel = inboxIntelligence(listedEmails);
|
||||
if (action === "summarize") {
|
||||
setAgentFocus(
|
||||
listedEmails.length
|
||||
? `${listedEmails.length} messages in ${category}. ${intel.actionItems} look like they need a reply, ${intel.meetings} mention a meeting, and ${intel.invoices} look like bills.`
|
||||
setAgentFocus("");
|
||||
runInsight({
|
||||
title: `${category} inbox`,
|
||||
summary: listedEmails.length
|
||||
? `${listedEmails.length} messages loaded. ${intel.actionItems} look like they need a reply.`
|
||||
: "Nothing in this view to summarize yet.",
|
||||
);
|
||||
details: [
|
||||
`${intel.meetings} mention meetings`,
|
||||
`${intel.invoices} look like bills`,
|
||||
`${intel.unread} unread overall`,
|
||||
],
|
||||
actions: ["Show action items", "Draft replies", "Find attachments"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action === "draft") {
|
||||
const first = listedEmails.find((email) => email.is_read === false) || listedEmails[0];
|
||||
setAgentFocus(first
|
||||
? `Draft a reply to ${first.subject || "the latest message"} when you open compose.`
|
||||
: "No message is waiting for a reply.");
|
||||
setAgentFocus("");
|
||||
runInsight({
|
||||
title: first?.subject || "Draft replies",
|
||||
summary: first
|
||||
? `Ready to draft a reply to "${first.subject || "the latest message"}".`
|
||||
: "No message is waiting for a reply.",
|
||||
details: first ? ["Tone: professional and concise", "Context pulled from inbox signals"] : [],
|
||||
actions: ["Open compose", "Suggest three reply options", "Improve existing draft"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action === "unread") {
|
||||
changeFilter("read", "unread");
|
||||
setFiltersOpen(true);
|
||||
setAgentFocus(`${intel.unread} unread messages in this mailbox.`);
|
||||
return;
|
||||
}
|
||||
if (action === "attachments" || action === "actions" || action === "important" || action === "actionItems" || action === "meetings" || action === "invoices") {
|
||||
|
|
@ -280,6 +411,21 @@ export default function MailboxView({
|
|||
}
|
||||
}
|
||||
|
||||
function handleCommand(action, query = "") {
|
||||
if (action === "search") {
|
||||
if (query.trim()) changeFilter("query", query.trim());
|
||||
window.setTimeout(() => document.getElementById("workspace-search")?.focus(), 0);
|
||||
return;
|
||||
}
|
||||
if (action === "from") {
|
||||
setFiltersOpen(true);
|
||||
if (query.trim()) changeFilter("sender", query.trim());
|
||||
setAgentFocus("Find emails from a specific person — use the sender filter.");
|
||||
return;
|
||||
}
|
||||
runQuickAction(action);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentNav || status.loading) return;
|
||||
if (agentNav === "integrations") {
|
||||
|
|
@ -364,9 +510,16 @@ export default function MailboxView({
|
|||
type="search"
|
||||
value={filters.query}
|
||||
onChange={(event) => changeFilter("query", event.target.value)}
|
||||
placeholder="Search emails, people, or ask your agent..."
|
||||
placeholder="Search emails, people, or ask your agent…"
|
||||
/>
|
||||
<kbd>Ctrl K</kbd>
|
||||
<button
|
||||
type="button"
|
||||
className="workspace-search-kbd"
|
||||
aria-label="Open command palette"
|
||||
onClick={() => setCommandOpen(true)}
|
||||
>
|
||||
<kbd>⌘K</kbd>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<div className="workspace-zone workspace-zone-actions">
|
||||
|
|
@ -379,8 +532,8 @@ export default function MailboxView({
|
|||
>
|
||||
<Filter size={16} />
|
||||
</button>
|
||||
<button className="workspace-ask" type="button" onClick={() => runQuickAction("summarize")}>
|
||||
<Sparkles size={15} /> Ask Agent
|
||||
<button className="workspace-ask agent-action" type="button" onClick={() => runQuickAction("summarize")}>
|
||||
<AgentSpark /> Ask Agent
|
||||
</button>
|
||||
</div>
|
||||
<div className="workspace-zone workspace-zone-account">
|
||||
|
|
@ -403,7 +556,24 @@ export default function MailboxView({
|
|||
<div className="mailbox-main-column">
|
||||
<div className={`mailbox-shell${selected ? " detail-visible" : ""}`}>
|
||||
<div className="mailbox-list-pane">
|
||||
{mailbox === "inbox" && (
|
||||
{selectedCount > 0 && (
|
||||
<MailboxSelectionBar
|
||||
count={selectedCount}
|
||||
total={listedEmails.length}
|
||||
onToggleAll={toggleSelectAll}
|
||||
onArchive={() => runBulkAction("archive")}
|
||||
onSpam={() => runBulkAction("spam")}
|
||||
onDelete={() => runBulkAction("delete")}
|
||||
onMarkUnread={() => runBulkAction("unread")}
|
||||
onSnooze={() => runBulkAction("snooze")}
|
||||
onMove={() => runBulkAction("move")}
|
||||
onLabel={() => runBulkAction("label")}
|
||||
onMore={() => runBulkAction("more")}
|
||||
onSummarize={summarizeSelection}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mailbox === "inbox" && selectedCount === 0 && (
|
||||
<div className="mailbox-categories" role="tablist" aria-label="Inbox categories">
|
||||
{CATEGORY_TABS.map(({ name, icon: Icon }) => (
|
||||
<button
|
||||
|
|
@ -414,7 +584,7 @@ export default function MailboxView({
|
|||
className={category === name ? "active" : ""}
|
||||
onClick={() => setCategory(name)}
|
||||
>
|
||||
<Icon size={14} strokeWidth={1.75} />
|
||||
<Icon size={16} strokeWidth={1.75} />
|
||||
<span className="mailbox-category-label">{name}</span>
|
||||
{name !== "All Mail" && (
|
||||
<span className={`mailbox-category-count${counts[name] > 0 ? "" : " muted"}`}>
|
||||
|
|
@ -507,6 +677,9 @@ export default function MailboxView({
|
|||
onTrash={() => mutateSelected(trashEmail)}
|
||||
onRestore={() => mutateSelected(restoreEmail)}
|
||||
onDelete={deletePermanently}
|
||||
onExtractActions={() => detail && extractActions(detail)}
|
||||
onSummarizeThread={() => detail && summarizeThread(detail)}
|
||||
onAnalyzeAttachment={() => detail && analyzeAttachment(detail)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -517,11 +690,22 @@ export default function MailboxView({
|
|||
account={account}
|
||||
emails={listedEmails}
|
||||
focus={agentFocus}
|
||||
insight={insight}
|
||||
onQuickAction={runQuickAction}
|
||||
onOpenEmail={(emailId) => {
|
||||
const email = listedEmails.find((item) => item.id === emailId);
|
||||
if (email) openEmail(email);
|
||||
}}
|
||||
/>
|
||||
<StickyNotesPanel compact />
|
||||
<StickyNotesPanel compact contextEmail={detail} />
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<CommandPalette
|
||||
open={commandOpen}
|
||||
onClose={() => setCommandOpen(false)}
|
||||
onCommand={handleCommand}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import {
|
||||
ChevronDown,
|
||||
MoreHorizontal,
|
||||
Pin,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
createStickyNote,
|
||||
deleteStickyNote,
|
||||
|
|
@ -14,6 +15,7 @@ import bluePin from "../data/blue.png";
|
|||
import greenPin from "../data/green.png";
|
||||
import pinkPin from "../data/pink.png";
|
||||
import yellowPin from "../data/yellow.png";
|
||||
import { senderDisplayName } from "../utils/senderAvatar";
|
||||
|
||||
const NOTE_COLORS = ["yellow", "green", "blue", "pink"];
|
||||
const PIN_IMAGES = {
|
||||
|
|
@ -35,6 +37,16 @@ function formatNoteTime(value) {
|
|||
}).format(date);
|
||||
}
|
||||
|
||||
function noteMatchesContext(note, contextEmail) {
|
||||
const needle = senderDisplayName(contextEmail).toLowerCase();
|
||||
if (!needle) return false;
|
||||
return (
|
||||
note.title.toLowerCase().includes(needle)
|
||||
|| note.body.toLowerCase().includes(needle)
|
||||
|| note.tags.some((tag) => tag.toLowerCase().includes(needle))
|
||||
);
|
||||
}
|
||||
|
||||
function NoteCard({ note, onDelete, menuOpen, onMenuToggle }) {
|
||||
return (
|
||||
<article className={`sticky-note sticky-note-${note.color}`}>
|
||||
|
|
@ -78,11 +90,12 @@ function NoteCard({ note, onDelete, menuOpen, onMenuToggle }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default function StickyNotesPanel({ compact = false }) {
|
||||
export default function StickyNotesPanel({ compact = false, contextEmail = null }) {
|
||||
const token = sessionStorage.getItem("agmail_access_token");
|
||||
const [notes, setNotes] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [menuOpenId, setMenuOpenId] = useState(null);
|
||||
const [draft, setDraft] = useState({
|
||||
|
|
@ -93,6 +106,16 @@ export default function StickyNotesPanel({ compact = false }) {
|
|||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const contextualNotes = useMemo(() => {
|
||||
if (!contextEmail) return [];
|
||||
return notes.filter((note) => noteMatchesContext(note, contextEmail));
|
||||
}, [contextEmail, notes]);
|
||||
|
||||
const visibleNotes = useMemo(() => {
|
||||
if (contextEmail && contextualNotes.length) return contextualNotes;
|
||||
return notes;
|
||||
}, [contextEmail, contextualNotes, notes]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
|
@ -144,6 +167,7 @@ export default function StickyNotesPanel({ compact = false }) {
|
|||
color: NOTE_COLORS[(notes.length + 1) % NOTE_COLORS.length],
|
||||
});
|
||||
setAdding(false);
|
||||
setExpanded(true);
|
||||
} catch (createError) {
|
||||
setError(createError.message);
|
||||
} finally {
|
||||
|
|
@ -177,14 +201,37 @@ export default function StickyNotesPanel({ compact = false }) {
|
|||
});
|
||||
}
|
||||
|
||||
const contextLabel = contextEmail ? senderDisplayName(contextEmail) : null;
|
||||
|
||||
return (
|
||||
<aside className={`mailbox-notes-pane${compact ? " compact" : ""}`} aria-label="Sticky notes">
|
||||
<header className="sticky-notes-header">
|
||||
<div className="sticky-notes-title">
|
||||
<Pin size={15} />
|
||||
<h2>Sticky Notes</h2>
|
||||
</div>
|
||||
<div className="sticky-notes-actions">
|
||||
<aside className={`mailbox-notes-pane${compact ? " compact" : ""}${expanded ? " expanded" : ""}`} aria-label="Sticky notes">
|
||||
<button
|
||||
type="button"
|
||||
className="sticky-notes-toggle"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setExpanded((open) => !open)}
|
||||
>
|
||||
<Pin size={14} />
|
||||
<span>
|
||||
Notes ({notes.length})
|
||||
{contextLabel && contextualNotes.length > 0 && (
|
||||
<small> · {contextualNotes.length} about {contextLabel}</small>
|
||||
)}
|
||||
</span>
|
||||
<ChevronDown size={15} className={expanded ? "open" : ""} />
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="sticky-notes-body">
|
||||
{contextLabel && (
|
||||
<p className="sticky-notes-context">
|
||||
{contextualNotes.length
|
||||
? `Notes about ${contextLabel}`
|
||||
: `No notes linked to ${contextLabel} yet.`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="sticky-notes-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Add sticky note"
|
||||
|
|
@ -192,21 +239,20 @@ export default function StickyNotesPanel({ compact = false }) {
|
|||
setDraft((current) => ({
|
||||
...current,
|
||||
color: NOTE_COLORS[notes.length % NOTE_COLORS.length],
|
||||
tags: contextLabel && !current.tags ? contextLabel : current.tags,
|
||||
}));
|
||||
setAdding(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={18} />
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="sticky-notes-body">
|
||||
{loading && <div className="sticky-notes-state">Loading notes…</div>}
|
||||
{error && !loading && (
|
||||
<div className="sticky-notes-state error" role="alert">{error}</div>
|
||||
)}
|
||||
{!loading && notes.map((note) => (
|
||||
{!loading && visibleNotes.map((note) => (
|
||||
<NoteCard
|
||||
key={note.id}
|
||||
note={note}
|
||||
|
|
@ -268,16 +314,17 @@ export default function StickyNotesPanel({ compact = false }) {
|
|||
setDraft((current) => ({
|
||||
...current,
|
||||
color: NOTE_COLORS[notes.length % NOTE_COLORS.length],
|
||||
tags: contextLabel || current.tags,
|
||||
}));
|
||||
setAdding(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={20} />
|
||||
<span>Add sticky note</span>
|
||||
<small>Jot down a quick note…</small>
|
||||
<Plus size={18} />
|
||||
<span>Add note</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
51
frontend/src/hooks/useAgentInsight.js
Normal file
51
frontend/src/hooks/useAgentInsight.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export function useAgentInsight() {
|
||||
const [insight, setInsight] = useState(null);
|
||||
const timers = useRef([]);
|
||||
|
||||
const clearTimers = useCallback(() => {
|
||||
timers.current.forEach((timer) => window.clearTimeout(timer));
|
||||
timers.current = [];
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => clearTimers(), [clearTimers]);
|
||||
|
||||
const runInsight = useCallback((payload) => {
|
||||
clearTimers();
|
||||
const { title, summary, details = [], actions = [] } = payload;
|
||||
|
||||
setInsight({
|
||||
processing: true,
|
||||
phase: "understanding",
|
||||
title,
|
||||
summary: null,
|
||||
details: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
timers.current.push(window.setTimeout(() => {
|
||||
setInsight((current) => current ? { ...current, phase: "summary", summary } : current);
|
||||
}, 520));
|
||||
|
||||
timers.current.push(window.setTimeout(() => {
|
||||
setInsight((current) => current ? { ...current, phase: "details", details } : current);
|
||||
}, 960));
|
||||
|
||||
timers.current.push(window.setTimeout(() => {
|
||||
setInsight((current) => current ? {
|
||||
...current,
|
||||
phase: "done",
|
||||
processing: false,
|
||||
actions,
|
||||
} : current);
|
||||
}, 1400));
|
||||
}, [clearTimers]);
|
||||
|
||||
const clearInsight = useCallback(() => {
|
||||
clearTimers();
|
||||
setInsight(null);
|
||||
}, [clearTimers]);
|
||||
|
||||
return { insight, runInsight, clearInsight };
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@400;500;600;700&display=swap");
|
||||
|
||||
:root { font-family:"DM Sans",sans-serif; color:#080821; background:#fffdfb; font-synthesis:none; --ink:#080821; --muted:#67677a; --coral:#ff806c; --blush:#fff3ef; --line:#eee9e5; }
|
||||
:root { font-family:"DM Sans",sans-serif; color:#080821; background:#fffdfb; font-synthesis:none; --ink:#080821; --muted:#67677a; --coral:#ff6a52; --blush:#fff3ef; --line:#eee9e5; --agent:#7c5cff; --agent-soft:#f3efff; --agent-border:#d8ccff; --info:#4d74c4; --info-soft:#eef4ff; --success:#3d8a5d; --success-soft:#eef8f2; --attention:#b7892d; --attention-soft:#fff8ea; --navy:#1b162d; --space-1:4px; --space-2:8px; --space-3:12px; --space-4:16px; --space-5:20px; --space-6:24px; --space-7:32px; --text-content:14px; --text-meta:12px; --text-control:13px; --line-content:1.45; --line-meta:1.4; --radius-xs:6px; --radius-sm:8px; --radius-md:10px; --radius-lg:12px; --radius-xl:16px; --radius-float:20px; --radius-pill:999px; --shadow-surface:0 1px 3px #08082108,0 10px 28px #0808210a; --shadow-row:0 1px 0 #ffffffb8 inset,0 2px 10px #0808210a; --icon-stroke:1.75; --motion-fast:140ms; --motion-base:180ms; --motion-slow:200ms; --ease-standard:cubic-bezier(.2,.8,.2,1); }
|
||||
* { box-sizing:border-box; }
|
||||
html { scroll-behavior:smooth; }
|
||||
body { margin:0; min-width:320px; }
|
||||
|
|
@ -290,8 +290,8 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
|||
.mail-detail-meta time { white-space:nowrap; }
|
||||
.mail-detail-body { padding-top:23px; color:#383440; font-size:12px; line-height:1.75; white-space:pre-wrap; overflow-wrap:anywhere; }
|
||||
|
||||
.compose-overlay { position:fixed; inset:0; z-index:40; padding:24px; display:flex; align-items:center; justify-content:center; background:#08082175; backdrop-filter:blur(5px); }
|
||||
.compose-modal { width:min(760px,100%); max-height:calc(100vh - 48px); overflow:auto; border:1px solid #eee7e2; border-radius:22px; background:#fffefd; box-shadow:0 32px 90px #08082150; }
|
||||
.compose-overlay { position:fixed; inset:0; z-index:40; padding:var(--space-6); display:flex; align-items:center; justify-content:center; background:#08082175; backdrop-filter:blur(5px); animation:fadeIn var(--motion-base) var(--ease-standard); }
|
||||
.compose-modal { width:min(760px,100%); max-height:calc(100vh - 48px); overflow:auto; border:0; border-radius:var(--radius-float); background:#fffefd; box-shadow:0 32px 90px #08082150; animation:scaleIn var(--motion-slow) var(--ease-standard); }
|
||||
.compose-header { min-height:76px; padding:0 24px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--line); background:linear-gradient(100deg,#fff6f2,#fffefd); }
|
||||
.compose-header>div { display:flex; align-items:center; gap:11px; }
|
||||
.compose-header>div>span { width:34px; height:34px; display:grid; place-items:center; border-radius:11px; background:var(--ink); color:var(--coral); }
|
||||
|
|
@ -324,7 +324,9 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
|||
.compose-tools>span { margin-left:7px; align-self:center; color:#aaa4ac; font-size:8px; }
|
||||
.compose-ai { min-height:58px; margin:14px 0 10px; padding:8px 9px 8px 14px; display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:10px; border:1px solid #e9e4ef; border-radius:14px; background:linear-gradient(100deg,#f5f4fb,#fff6f2); }
|
||||
.compose-ai>svg { color:#7463a8; }
|
||||
.compose-ai button { min-height:35px; padding:0 13px; border:0; border-radius:9px; background:var(--ink); color:white; font-size:10px; font-weight:700; }
|
||||
.compose-ai button { min-height:35px; padding:0 13px; border:0; border-radius:var(--radius-sm); background:var(--ink); color:white; font-size:10px; font-weight:700; }
|
||||
.compose-ai button.agent-action { background:var(--agent-soft); color:#6d4aff; border:1px solid var(--agent-border); font-size:12px; }
|
||||
.compose-ai button.agent-action:hover:not(:disabled) { background:#ebe4ff; color:#5a3fe0; }
|
||||
.compose-ai button:disabled { opacity:.45; cursor:not-allowed; }
|
||||
.compose-error { margin:8px 0; color:#b54f3d; font-size:10px; }
|
||||
.compose-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; }
|
||||
|
|
@ -444,7 +446,7 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
|||
.dashboard-sidebar-section { margin-top:18px; padding-top:16px; border-top:1px solid #ffffff0a; }
|
||||
.dashboard-sidebar-section h2 { margin:0 0 6px; padding:0 8px; color:#6f6a7d; font-size:9px; font-weight:700; letter-spacing:1.4px; text-transform:uppercase; }
|
||||
.dashboard-sidebar-section nav { display:grid; gap:1px; }
|
||||
.dashboard-nav-item { min-height:32px; padding:0 8px; display:flex; align-items:center; gap:10px; border:0; border-radius:9px; background:transparent; color:#b9b4c6; font-size:12.5px; font-weight:500; text-align:left; transition:background .14s ease,color .14s ease,box-shadow .14s ease; }
|
||||
.dashboard-nav-item { min-height:36px; padding:0 var(--space-2); display:flex; align-items:center; gap:var(--space-3); border:0; border-radius:var(--radius-sm); background:transparent; color:#b9b4c6; font-size:var(--text-meta); font-weight:500; text-align:left; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard),box-shadow var(--motion-fast) var(--ease-standard); }
|
||||
.dashboard-nav-item svg { flex-shrink:0; opacity:.92; }
|
||||
.dashboard-nav-item span { flex:1; min-width:0; }
|
||||
.dashboard-nav-item em { margin-left:auto; min-width:17px; height:17px; padding:0 5px; display:grid; place-items:center; border-radius:99px; background:#ff6a52; color:#fff; font-size:9px; font-style:normal; font-weight:700; line-height:1; box-shadow:0 1px 4px #ff6a5238; }
|
||||
|
|
@ -467,32 +469,55 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
|||
.dashboard-profile button:hover { background:#ffffff12; color:#fff; }
|
||||
.dashboard-content { padding:14px 16px 16px; background:#e4e1dc; }
|
||||
.dashboard-content-heading h1 { font-size:clamp(28px,3vw,36px); letter-spacing:-1.4px; }
|
||||
.mailbox-workspace { min-height:calc(100vh - 28px); display:flex; flex-direction:column; gap:12px; }
|
||||
.workspace-header { min-height:58px; display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:0 20px; align-items:center; }
|
||||
.mailbox-workspace { min-height:calc(100vh - 28px); display:flex; flex-direction:column; gap:var(--space-3); }
|
||||
.workspace-header { min-height:56px; display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:0 var(--space-5); align-items:center; }
|
||||
.workspace-zone { display:flex; align-items:center; min-width:0; }
|
||||
.workspace-zone-command { padding-right:8px; }
|
||||
.workspace-zone-command .workspace-search { width:min(100%,620px); max-width:620px; }
|
||||
.workspace-zone-actions { gap:10px; padding:0 16px; border-left:1px solid #d5d0ca; border-right:1px solid #d5d0ca; }
|
||||
.workspace-zone-account { gap:6px; padding-left:4px; }
|
||||
.workspace-search { height:40px; min-width:0; padding:0 8px 0 14px; display:flex; align-items:center; gap:8px; border:1px solid #d5d0ca; border-radius:12px; background:#f8f7f5; color:#9a958f; box-shadow:none; }
|
||||
.workspace-search input { min-width:0; width:100%; border:0; outline:0; background:transparent; color:#241f2b; font-size:13px; }
|
||||
.workspace-search kbd { padding:3px 7px; border:1px solid #e8e3de; border-radius:7px; background:#f2f0ec; color:#8d8792; font:600 10px "DM Sans"; white-space:nowrap; }
|
||||
.workspace-filter,.workspace-icon { width:36px; height:36px; display:grid; place-items:center; border:0; border-radius:10px; background:transparent; color:#6f6a74; }
|
||||
.workspace-zone-actions { gap:var(--space-2); padding:0 var(--space-4); border-left:1px solid #d5d0ca; border-right:1px solid #d5d0ca; }
|
||||
.workspace-zone-account { gap:var(--space-2); padding-left:var(--space-1); }
|
||||
.workspace-search { height:40px; min-width:0; padding:0 var(--space-2) 0 var(--space-4); display:flex; align-items:center; gap:var(--space-2); border:1px solid #d5d0ca; border-radius:var(--radius-lg); background:#f8f7f5; color:#9a958f; box-shadow:none; transition:border-color var(--motion-fast) var(--ease-standard),box-shadow var(--motion-fast) var(--ease-standard); }
|
||||
.workspace-search:focus-within { border-color:#d8ccff; box-shadow:0 0 0 3px #7c5cff14; }
|
||||
.workspace-search input { min-width:0; width:100%; border:0; outline:0; background:transparent; color:#241f2b; font-size:var(--text-control); }
|
||||
.workspace-search kbd { padding:var(--space-1) var(--space-2); border:1px solid #e8e3de; border-radius:var(--radius-xs); background:#f2f0ec; color:#8d8792; font:600 var(--text-meta) "DM Sans"; white-space:nowrap; }
|
||||
.workspace-filter,.workspace-icon { width:40px; height:40px; display:grid; place-items:center; border:0; border-radius:var(--radius-md); background:transparent; color:#6f6a74; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
|
||||
.workspace-filter.active,.workspace-icon:hover,.workspace-filter:hover { background:#f8f6f3; color:#241f2b; }
|
||||
.workspace-ask { height:40px; padding:0 16px; display:inline-flex; align-items:center; gap:8px; border:1px solid #ffb09c; border-radius:11px; background:linear-gradient(180deg,#fff8f5 0%,#ffefe8 100%); color:#cf4f36; font-size:13px; font-weight:700; white-space:nowrap; box-shadow:0 2px 10px #ff6a5224, inset 0 1px 0 #ffffffaa; }
|
||||
.workspace-ask:hover { background:linear-gradient(180deg,#fff3ee 0%,#ffe8df 100%); box-shadow:0 3px 12px #ff6a5230; }
|
||||
.workspace-account { margin-left:2px; padding:4px 8px 4px 4px; display:flex; align-items:center; gap:8px; border:0; border-radius:12px; background:transparent; color:#6d6872; }
|
||||
.workspace-ask { height:40px; padding:0 var(--space-4); display:inline-flex; align-items:center; gap:var(--space-2); border:1px solid var(--agent-border); border-radius:var(--radius-sm); background:linear-gradient(180deg,#faf7ff 0%,#f3efff 100%); color:#6d4aff; font-size:var(--text-control); font-weight:700; white-space:nowrap; box-shadow:0 2px 10px #7c5cff1a, inset 0 1px 0 #ffffffaa; transition:background var(--motion-base) var(--ease-standard),box-shadow var(--motion-base) var(--ease-standard),transform var(--motion-fast) var(--ease-standard); }
|
||||
.workspace-ask:hover { background:linear-gradient(180deg,#f5f0ff 0%,#ebe4ff 100%); box-shadow:0 3px 12px #7c5cff24; }
|
||||
.agent-action { display:inline-flex; align-items:center; gap:var(--space-2); border:1px solid var(--agent-border); border-radius:var(--radius-sm); background:var(--agent-soft); color:#6d4aff; font-weight:600; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard),border-color var(--motion-fast) var(--ease-standard); }
|
||||
.agent-action:hover { background:#ebe4ff; color:#5a3fe0; }
|
||||
.agent-action.subtle { border-color:transparent; background:transparent; color:#7b67c8; font-weight:600; }
|
||||
.agent-action.subtle:hover { background:var(--agent-soft); color:#5a3fe0; }
|
||||
.agent-spark { display:inline-flex; align-items:center; justify-content:center; color:var(--agent); font-size:12px; line-height:1; font-weight:700; }
|
||||
.agent-spark.pulse { animation:agentSparkPulse 1.2s ease-in-out infinite; }
|
||||
.agent-label { display:inline-flex; align-items:center; gap:var(--space-1); }
|
||||
.agent-insight { margin:0 0 var(--space-3); display:grid; gap:var(--space-2); }
|
||||
.agent-insight-status { margin:0; display:flex; align-items:center; gap:var(--space-2); color:#6d4aff; font-size:var(--text-meta); font-weight:600; line-height:var(--line-meta); }
|
||||
.agent-insight-block { padding:var(--space-3); border-radius:var(--radius-md); background:#fff; box-shadow:0 1px 2px #08082108; }
|
||||
.agent-insight-block h4 { margin:0 0 var(--space-1); display:flex; align-items:center; gap:var(--space-1); color:#5a3fe0; font-size:var(--text-meta); font-weight:700; letter-spacing:.2px; text-transform:uppercase; }
|
||||
.agent-insight-block p,.agent-insight-block li { margin:0; color:#4a454f; font-size:var(--text-content); line-height:var(--line-content); }
|
||||
.agent-insight-block ul { margin:0; padding:0; list-style:none; display:grid; gap:var(--space-1); }
|
||||
.agent-insight-actions li { display:flex; align-items:center; gap:var(--space-1); color:#6d4aff; font-size:var(--text-meta); font-weight:600; }
|
||||
.agent-insight-block.reveal { animation:agentReveal var(--motion-slow) var(--ease-standard) both; }
|
||||
.agent-insight-block.reveal.delay-1 { animation-delay:80ms; }
|
||||
.agent-insight-block.reveal.delay-2 { animation-delay:160ms; }
|
||||
@keyframes agentSparkPulse { 0%,100% { opacity:.55; transform:scale(.92); } 50% { opacity:1; transform:scale(1); } }
|
||||
@keyframes agentReveal { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
|
||||
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
|
||||
@keyframes scaleIn { from { opacity:0; transform:translateY(8px) scale(.98); } to { opacity:1; transform:translateY(0) scale(1); } }
|
||||
@keyframes slideInDetail { from { opacity:0; transform:translateX(12px); } to { opacity:1; transform:translateX(0); } }
|
||||
.workspace-account { margin-left:2px; padding:var(--space-1) var(--space-2) var(--space-1) var(--space-1); display:flex; align-items:center; gap:var(--space-2); border:0; border-radius:var(--radius-lg); background:transparent; color:#6d6872; transition:background var(--motion-fast) var(--ease-standard); }
|
||||
.workspace-account:hover { background:#f8f6f3; }
|
||||
.workspace-account>span { width:34px; height:34px; display:grid; place-items:center; border-radius:50%; background:#6d4aff; color:#fff; font-size:11px; font-weight:700; }
|
||||
.workspace-account div { min-width:0; display:grid; text-align:left; }
|
||||
.workspace-account strong,.workspace-account small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.workspace-account strong { color:#241f2b; font-size:12px; }
|
||||
.workspace-account small { color:#8d8792; font-size:10px; max-width:150px; }
|
||||
.mailbox-stage { min-height:0; flex:1; display:grid; grid-template-columns:minmax(0,1fr) minmax(268px,286px); gap:16px; align-items:start; }
|
||||
.mailbox-shell { min-height:0; max-height:calc(100vh - 110px); margin-top:0; overflow:hidden; border:1px solid #dbd6d0; border-radius:12px; background:#f0eeea; box-shadow:0 1px 2px #08082105; }
|
||||
.mailbox-list-pane { min-height:0; max-height:calc(100vh - 110px); display:flex; flex-direction:column; background:#f0eeea; border-radius:12px; }
|
||||
.mailbox-stage { min-height:0; flex:1; display:grid; grid-template-columns:minmax(0,1fr) minmax(268px,286px); gap:var(--space-4); align-items:start; }
|
||||
.mailbox-shell { min-height:0; max-height:calc(100vh - 110px); margin-top:0; overflow:hidden; border:0; border-radius:var(--radius-xl); background:#f0eeea; box-shadow:var(--shadow-surface); }
|
||||
.mailbox-list-pane { min-height:0; max-height:calc(100vh - 110px); display:flex; flex-direction:column; background:#f0eeea; border-radius:var(--radius-xl); }
|
||||
.mailbox-categories { min-height:42px; padding:4px 8px 0; display:flex; align-items:flex-end; gap:2px; border-bottom:1px solid #ddd8d2; background:#e8e5e0; overflow:auto; }
|
||||
.mailbox-categories button { height:38px; padding:0 10px 8px; position:relative; display:inline-flex; align-items:center; gap:6px; border:0; border-radius:8px 8px 0 0; background:transparent; color:#6b666f; font-size:12px; font-weight:600; white-space:nowrap; }
|
||||
.mailbox-categories button { height:40px; padding:0 var(--space-3) var(--space-2); position:relative; display:inline-flex; align-items:center; gap:var(--space-2); border:0; border-radius:var(--radius-sm) var(--radius-sm) 0 0; background:transparent; color:#6b666f; font-size:var(--text-meta); font-weight:600; white-space:nowrap; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
|
||||
.mailbox-categories button svg { color:#8a858f; flex-shrink:0; }
|
||||
.mailbox-category-label { line-height:1; }
|
||||
.mailbox-category-count { min-width:17px; height:17px; padding:0 5px; display:grid; place-items:center; border-radius:99px; background:#ff6a52; color:#fff; font-size:9px; font-weight:700; line-height:1; }
|
||||
|
|
@ -501,50 +526,65 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
|||
.mailbox-categories button.active svg { color:#e15b45; }
|
||||
.mailbox-categories button.active:after { content:""; position:absolute; left:8px; right:8px; bottom:0; height:3px; border-radius:3px 3px 0 0; background:linear-gradient(90deg,#ff8a72,#ff6a52); }
|
||||
.mailbox-range { margin-left:auto; padding-right:8px; color:#9a948e; font-size:10px; font-weight:500; white-space:nowrap; }
|
||||
.mailbox-selection-bar { min-height:42px; padding:6px 10px; display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:10px; border-bottom:1px solid #ddd8d2; background:#f3f1ed; }
|
||||
.mailbox-selection-meta { display:flex; align-items:center; gap:8px; color:#4a4652; font-size:12px; font-weight:600; white-space:nowrap; }
|
||||
.mailbox-selection-meta input { width:15px; height:15px; margin:0; accent-color:var(--coral); }
|
||||
.mailbox-selection-actions { display:flex; flex-wrap:wrap; align-items:center; gap:4px; }
|
||||
.mailbox-selection-actions button { height:32px; padding:0 var(--space-2); display:inline-flex; align-items:center; gap:var(--space-2); border:0; border-radius:var(--radius-sm); background:transparent; color:#5a5562; font-size:var(--text-meta); font-weight:600; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
|
||||
.mailbox-selection-actions button:hover { background:#ebe8e3; color:#241f2b; }
|
||||
.mailbox-selection-summarize { height:36px; padding:0 var(--space-3); white-space:nowrap; font-size:var(--text-meta); }
|
||||
.mailbox-rows { flex:1; min-height:0; overflow:auto; background:#ebe8e3; }
|
||||
.mailbox-row { width:100%; min-height:44px; padding:0 10px 0 8px; display:grid; grid-template-columns:15px 18px 30px minmax(0,1fr) auto; align-items:center; gap:7px; border:0; border-bottom:1px solid #00000007; background:transparent; color:#4a4652; text-align:left; cursor:pointer; transition:background .12s ease; }
|
||||
.mailbox-row { width:100%; min-height:40px; padding:0 var(--space-3) 0 var(--space-2); display:grid; grid-template-columns:16px 20px 32px minmax(0,1fr) minmax(120px,auto); align-items:center; gap:var(--space-2); position:relative; border:0; border-bottom:1px solid #00000007; background:transparent; color:#4a4652; text-align:left; cursor:pointer; transition:background var(--motion-fast) var(--ease-standard),box-shadow var(--motion-fast) var(--ease-standard); }
|
||||
.mailbox-row.unread { background:#faf9f7; }
|
||||
.mailbox-row:not(.unread) { background:transparent; }
|
||||
.mailbox-row:hover { background:#e3e0db; }
|
||||
.mailbox-row.selected { background:#fff1ec; box-shadow:inset 3px 0 0 #ff7a62; }
|
||||
.mailbox-check { width:14px; height:14px; margin:0; accent-color:#ff6a52; }
|
||||
.mailbox-star { width:18px; height:18px; padding:0; display:grid; place-items:center; border:0; border-radius:6px; background:transparent; color:#c2bdc6; }
|
||||
.mailbox-row.unread .mailbox-unread-pip { opacity:1; }
|
||||
.mailbox-row:not(.unread) .mailbox-sender,.mailbox-row:not(.unread) .mailbox-subject { color:#5a5562; font-weight:500; }
|
||||
.mailbox-row.unread .mailbox-sender,.mailbox-row.unread .mailbox-subject { color:#1a1628; font-weight:600; }
|
||||
.mailbox-row:hover { z-index:1; background:#f5f3f0; box-shadow:var(--shadow-row); }
|
||||
.mailbox-row.checked { background:#f7f5ff; }
|
||||
.mailbox-row.checked .mailbox-check { opacity:1; transform:scale(1); }
|
||||
.mailbox-row.selected { background:#fff6f3; box-shadow:inset 2px 0 0 var(--coral),var(--shadow-row); }
|
||||
.mailbox-row.checked.selected { background:#fff4f8; box-shadow:inset 2px 0 0 var(--coral),0 0 0 1px #7c5cff22 inset; }
|
||||
.mailbox-row.checked .mailbox-sender { font-weight:600; }
|
||||
.mailbox-check { width:16px; height:16px; margin:0; opacity:.72; accent-color:var(--coral); transition:opacity var(--motion-fast) var(--ease-standard),transform var(--motion-fast) var(--ease-standard); }
|
||||
.mailbox-row:hover .mailbox-check,.mailbox-row.checked .mailbox-check,.mailbox-row:focus-within .mailbox-check { opacity:1; }
|
||||
.mailbox-star { width:18px; height:18px; padding:0; display:grid; place-items:center; border:0; border-radius:var(--radius-xs); background:transparent; color:#c2bdc6; }
|
||||
.mailbox-star:hover,.mailbox-star.active { color:#e8a317; background:#fff8e8; }
|
||||
.mailbox-row.starred .mailbox-star { color:#e8a317; }
|
||||
.sender-avatar.mailbox-avatar { width:30px; height:30px; font-size:10px; font-weight:700; }
|
||||
.sender-avatar.mailbox-avatar { width:32px; height:32px; font-size:var(--text-meta); font-weight:700; }
|
||||
.sender-avatar-brand { background:#fff!important; border:1px solid #e8e4df; padding:4px; }
|
||||
.sender-avatar-brand img { object-fit:contain; }
|
||||
.mailbox-content { min-width:0; display:flex; align-items:center; gap:7px; overflow:hidden; }
|
||||
.mailbox-content { min-width:0; display:flex; align-items:center; gap:var(--space-2); overflow:hidden; }
|
||||
.mailbox-unread-pip { width:6px; height:6px; flex-shrink:0; border-radius:50%; background:#4f8cff; box-shadow:0 0 0 2px #4f8cff22; }
|
||||
.mailbox-sender { flex:0 1 auto; max-width:28%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12.5px; }
|
||||
.mailbox-subject { flex:0 1 auto; max-width:30%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12.5px; }
|
||||
.mailbox-preview { flex:1 1 auto; min-width:48px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12.5px; font-weight:400; color:#9a959f; }
|
||||
.mailbox-preview-sep { margin-right:4px; color:#c2bdc6; }
|
||||
.mailbox-meta { flex:0 0 auto; display:inline-flex; align-items:center; gap:4px; }
|
||||
.mailbox-row.unread .mailbox-sender,.mailbox-row.unread .mailbox-subject { color:#1a1628; font-weight:600; }
|
||||
.mailbox-row:not(.unread) .mailbox-sender,.mailbox-row:not(.unread) .mailbox-subject { color:#5a5562; font-weight:500; }
|
||||
.mailbox-time { color:#a39ea8; font-size:10.5px; font-weight:400; white-space:nowrap; }
|
||||
.mail-label,.mail-attachment { padding:1px 6px; border-radius:99px; font-size:9px; font-style:normal; font-weight:600; white-space:nowrap; }
|
||||
.mailbox-sender { flex:0 1 auto; max-width:28%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:var(--text-content); line-height:var(--line-content); }
|
||||
.mailbox-subject { flex:0 1 auto; max-width:30%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:var(--text-content); line-height:var(--line-content); }
|
||||
.mailbox-preview { flex:1 1 auto; min-width:48px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:var(--text-meta); font-weight:400; line-height:var(--line-meta); color:#9a959f; }
|
||||
.mailbox-preview-sep { margin-right:var(--space-1); color:#c2bdc6; }
|
||||
.mailbox-meta { flex:0 0 auto; display:inline-flex; align-items:center; gap:var(--space-1); }
|
||||
.mailbox-time { color:#a39ea8; font-size:var(--text-meta); font-weight:400; line-height:var(--line-meta); white-space:nowrap; }
|
||||
.mail-label,.mail-attachment { padding:var(--space-1) var(--space-2); border-radius:var(--radius-pill); font-size:var(--text-meta); font-style:normal; font-weight:600; white-space:nowrap; }
|
||||
.mail-label-rose { background:#ffe8e3; color:#c45a48; }
|
||||
.mail-label-violet { background:#efe9ff; color:#6d54b8; }
|
||||
.mail-label-blue { background:#e7f1ff; color:#3d6eae; }
|
||||
.mail-label-amber { background:#fff4d8; color:#9a7420; }
|
||||
.mail-label-green { background:#e7f6ee; color:#2f7a52; }
|
||||
.mail-attachment { display:inline-flex; align-items:center; gap:2px; background:#e8e5e0; color:#7a7580; font-weight:500; }
|
||||
.mailbox-hover-actions { display:none; align-items:center; gap:1px; }
|
||||
.mailbox-row:hover .mailbox-hover-actions,.mailbox-row:focus-within .mailbox-hover-actions { display:flex; }
|
||||
.mailbox-row:hover .mailbox-time,.mailbox-row:focus-within .mailbox-time { display:none; }
|
||||
.mailbox-hover-actions button { width:26px; height:26px; display:grid; place-items:center; border:0; border-radius:7px; background:#f8f6f3; color:#6d6872; box-shadow:0 1px 2px #0808210c; }
|
||||
.mailbox-hover-actions button:hover { color:#e15b45; background:#fff4f1; }
|
||||
.mailbox-rail { min-width:0; display:flex; flex-direction:column; gap:12px; max-height:calc(100vh - 96px); position:sticky; top:14px; }
|
||||
.agent-panel { padding:16px; border:1px solid #e4e0da; border-radius:12px; background:#faf9f7; box-shadow:0 1px 2px #08082105; }
|
||||
.agent-panel-head { display:flex; align-items:center; gap:10px; }
|
||||
.agent-panel-mark { width:32px; height:32px; display:grid; place-items:center; border-radius:10px; background:#fff1eb; color:#e15b45; }
|
||||
.agent-panel-head h2 { margin:0; font:700 15px "Manrope"; letter-spacing:-.3px; }
|
||||
.agent-panel-head p { margin:1px 0 0; color:#8d8792; font-size:11px; }
|
||||
.agent-panel-hello { margin:14px 0 12px; display:grid; gap:3px; }
|
||||
.agent-panel-hello strong { font-size:15px; letter-spacing:-.2px; }
|
||||
.agent-panel-hello span,.agent-panel-hello p { margin:0; color:#7a746e; font-size:12px; line-height:1.45; }
|
||||
.mailbox-hover-actions { display:none; align-items:center; gap:2px; max-width:min(52vw,420px); overflow:hidden; }
|
||||
.mailbox-row:hover .mailbox-hover-actions,.mailbox-row:focus-within .mailbox-hover-actions,.mailbox-row.selected .mailbox-hover-actions { display:flex; }
|
||||
.mailbox-row:hover .mailbox-time,.mailbox-row:focus-within .mailbox-time,.mailbox-row.selected .mailbox-time { display:none; }
|
||||
.mailbox-hover-actions button { height:32px; min-width:32px; padding:0 var(--space-2); display:inline-flex; align-items:center; gap:var(--space-1); border:0; border-radius:var(--radius-xs); background:transparent; color:#6d6872; font-size:var(--text-meta); font-weight:600; white-space:nowrap; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
|
||||
.mailbox-hover-actions button:hover { color:#241f2b; background:#ffffffb8; }
|
||||
.mailbox-hover-actions button.agent-action { border:1px solid var(--agent-border); background:var(--agent-soft); color:#6d4aff; }
|
||||
.mailbox-hover-actions button.agent-action:hover { background:#ebe4ff; color:#5a3fe0; }
|
||||
.mailbox-hover-sep { color:#c8c3cc; font-size:10px; user-select:none; }
|
||||
.mailbox-rail { min-width:0; display:flex; flex-direction:column; gap:var(--space-3); max-height:calc(100vh - 96px); position:sticky; top:var(--space-3); }
|
||||
.agent-panel { padding:var(--space-4); border:0; border-radius:var(--radius-lg); background:#faf9f7; box-shadow:var(--shadow-surface); transition:box-shadow var(--motion-base) var(--ease-standard); }
|
||||
.agent-panel-head { display:flex; align-items:center; gap:var(--space-3); }
|
||||
.agent-panel-mark { width:36px; height:36px; display:grid; place-items:center; border-radius:var(--radius-md); background:var(--agent-soft); color:var(--agent); font-size:16px; }
|
||||
.agent-panel-head h2 { margin:0; font:700 16px "Manrope"; letter-spacing:-.3px; }
|
||||
.agent-panel-head p { margin:var(--space-1) 0 0; color:#8d8792; font-size:var(--text-meta); line-height:var(--line-meta); }
|
||||
.agent-panel-hello { margin:var(--space-4) 0 var(--space-3); display:grid; gap:var(--space-1); }
|
||||
.agent-panel-hello strong { font-size:var(--text-content); letter-spacing:-.2px; line-height:var(--line-content); }
|
||||
.agent-panel-hello span,.agent-panel-hello p { margin:0; color:#7a746e; font-size:var(--text-meta); line-height:var(--line-content); }
|
||||
.agent-stat-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
|
||||
.agent-stat { min-height:92px; padding:10px; display:grid; align-content:start; gap:2px; border:0; border-radius:12px; text-align:left; }
|
||||
.agent-stat strong { font:700 20px "Manrope"; letter-spacing:-.6px; color:#241f2b; }
|
||||
|
|
@ -557,13 +597,61 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
|||
.agent-quick { margin-top:14px; }
|
||||
.agent-quick h3 { margin:0 0 8px; color:#8d8792; font-size:11px; letter-spacing:.4px; text-transform:uppercase; }
|
||||
.agent-quick div { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
|
||||
.agent-quick button { min-height:36px; padding:0 8px; display:flex; align-items:center; justify-content:center; gap:6px; border:1px solid #eee8e3; border-radius:10px; background:#fff; color:#4d4852; font-size:11px; font-weight:600; }
|
||||
.agent-quick button:hover { border-color:#ffd3c8; color:#e15b45; background:#fff8f5; }
|
||||
.mailbox-notes-pane.compact { min-height:0; max-height:240px; border-radius:12px; border-color:#e4e0da; box-shadow:0 1px 2px #08082105; }
|
||||
.mailbox-notes-pane.compact .sticky-notes-header { min-height:44px; padding:8px 12px; }
|
||||
.mailbox-notes-pane.compact .sticky-notes-body { padding:10px; gap:8px; }
|
||||
.agent-quick button { min-height:36px; padding:0 var(--space-2); display:flex; align-items:center; justify-content:center; gap:var(--space-2); border:0; border-radius:var(--radius-sm); background:#fff; color:#4d4852; font-size:var(--text-meta); font-weight:600; box-shadow:0 1px 2px #08082108; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
|
||||
.agent-quick button:hover { color:#241f2b; background:#f8f6f3; }
|
||||
.agent-quick button.agent-action.subtle { box-shadow:none; }
|
||||
.agent-quick button.agent-action.subtle:hover { background:var(--agent-soft); color:#5a3fe0; }
|
||||
.agent-caught-up { margin:0 0 4px; padding:12px; display:flex; flex-wrap:wrap; align-items:center; gap:8px; border-radius:10px; background:#f3f1ee; color:#6f6a64; font-size:12px; }
|
||||
.agent-caught-up p { margin:0; flex:1; min-width:120px; }
|
||||
.agent-mini-metric { padding:4px 10px; border:1px solid #e4e0da; border-radius:999px; background:#fff; color:#5c5763; font-size:11px; font-weight:600; }
|
||||
.agent-mini-metric:hover { border-color:#ffd3c8; color:#e15b45; }
|
||||
.agent-context-block { margin-top:14px; padding-top:12px; border-top:1px solid #ece7e1; }
|
||||
.agent-context-block h3 { margin:0 0 8px; display:flex; align-items:center; gap:5px; color:#8d8792; font-size:11px; letter-spacing:.4px; text-transform:uppercase; }
|
||||
.agent-context-block ul { margin:0; padding:0; list-style:none; display:grid; gap:6px; }
|
||||
.agent-context-block li { display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:12px; color:#4a454f; }
|
||||
.agent-context-block time { min-width:42px; color:#e15b45; font-size:11px; font-weight:700; }
|
||||
.agent-context-block em { color:#8d8792; font-size:11px; font-style:normal; font-weight:600; }
|
||||
.agent-context-block button { width:100%; padding:6px 0; display:flex; align-items:center; justify-content:space-between; gap:8px; border:0; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
|
||||
.agent-context-block button:hover span { color:#e15b45; }
|
||||
.command-palette-overlay { position:fixed; inset:0; z-index:120; display:grid; place-items:start center; padding:12vh var(--space-4) var(--space-4); background:#120f1a66; backdrop-filter:blur(4px); animation:fadeIn var(--motion-base) var(--ease-standard); }
|
||||
.command-palette { width:min(560px,100%); border:0; border-radius:var(--radius-float); background:#faf9f7; box-shadow:0 24px 60px #08082133; overflow:hidden; animation:scaleIn var(--motion-slow) var(--ease-standard); }
|
||||
.command-palette-input { display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid #ece7e1; }
|
||||
.command-palette-input input { flex:1; border:0; background:transparent; color:#2a2530; font:500 15px "DM Sans"; outline:none; }
|
||||
.command-palette-input input::placeholder { color:#9a949e; }
|
||||
.command-palette-input svg { color:#9a949e; flex-shrink:0; }
|
||||
.command-palette-input kbd { padding:2px 6px; border:1px solid #e4e0da; border-radius:6px; background:#fff; color:#8d8792; font-size:10px; }
|
||||
.command-palette-list { margin:0; padding:6px; list-style:none; max-height:320px; overflow:auto; }
|
||||
.command-palette-list button { width:100%; padding:10px 12px; display:flex; align-items:flex-start; gap:10px; border:0; border-radius:10px; background:transparent; color:#3d3944; text-align:left; cursor:pointer; }
|
||||
.command-palette-list button strong { display:block; font-size:13px; font-weight:600; }
|
||||
.command-palette-list button small { display:block; margin-top:1px; color:#8d8792; font-size:11px; font-weight:400; }
|
||||
.command-palette-list button.active,.command-palette-list button:hover { background:#fff1eb; color:#c24f3a; }
|
||||
.command-palette-list button.active small,.command-palette-list button:hover small { color:#b76f60; }
|
||||
.command-palette-empty { padding:18px 12px; color:#8d8792; font-size:12px; text-align:center; }
|
||||
.command-palette-foot { padding:10px 14px; display:flex; flex-wrap:wrap; gap:12px; border-top:1px solid #ece7e1; color:#9a949e; font-size:10px; }
|
||||
.command-palette-foot kbd { margin-right:4px; padding:1px 5px; border:1px solid #e4e0da; border-radius:5px; background:#fff; }
|
||||
.workspace-search-kbd { padding:0; border:0; background:transparent; cursor:pointer; }
|
||||
.workspace-search-kbd kbd { pointer-events:none; }
|
||||
.mail-detail-agent-bar { margin-bottom:16px; display:flex; flex-wrap:wrap; gap:6px; }
|
||||
.mail-detail.open { animation:slideInDetail var(--motion-slow) var(--ease-standard); }
|
||||
.mail-detail-agent-bar .agent-action.subtle { height:36px; padding:0 var(--space-3); font-size:var(--text-meta); border-radius:var(--radius-sm); }
|
||||
.mail-detail-content h2 { margin-top:0; }
|
||||
.compose-agent-mark { width:34px; height:34px; display:grid; place-items:center; border-radius:var(--radius-md); background:var(--agent-soft); color:var(--agent); }
|
||||
.compose-ai .agent-action { height:36px; padding:0 12px; font-size:12px; white-space:nowrap; }
|
||||
.mailbox-notes-pane.compact { min-height:0; border-radius:var(--radius-lg); border:0; background:#faf9f7; box-shadow:0 1px 2px #08082108; overflow:hidden; }
|
||||
.mailbox-notes-pane.compact.expanded { max-height:min(42vh,320px); display:flex; flex-direction:column; }
|
||||
.mailbox-notes-pane.compact.expanded .sticky-notes-body { animation:agentReveal var(--motion-slow) var(--ease-standard); }
|
||||
.sticky-notes-toggle { width:100%; min-height:40px; padding:0 12px; display:flex; align-items:center; gap:8px; border:0; background:transparent; color:#5c5763; font:600 12px "DM Sans"; cursor:pointer; }
|
||||
.sticky-notes-toggle svg:first-child { transform:rotate(-24deg); color:#bd5947; }
|
||||
.sticky-notes-toggle span { flex:1; text-align:left; }
|
||||
.sticky-notes-toggle span small { color:#8d8792; font-weight:500; }
|
||||
.sticky-notes-toggle .open { transform:rotate(180deg); }
|
||||
.mailbox-notes-pane.compact .sticky-notes-body { flex:1; padding:0 10px 10px; display:flex; flex-direction:column; gap:8px; overflow:auto; }
|
||||
.sticky-notes-context { margin:0; padding:0 2px; color:#8d8792; font-size:11px; }
|
||||
.sticky-notes-toolbar { display:flex; justify-content:flex-end; }
|
||||
.sticky-notes-toolbar button { width:30px; height:30px; padding:0; display:grid; place-items:center; border:0; border-radius:8px; background:#fff; color:#77717a; box-shadow:0 1px 2px #0808210c; cursor:pointer; }
|
||||
.sticky-notes-toolbar button:hover { color:#bd5947; background:#fff0eb; }
|
||||
.mailbox-notes-pane.compact .sticky-note { height:92px; min-height:92px; padding:12px 12px 8px; }
|
||||
.mailbox-notes-pane.compact .sticky-note-add { min-height:64px; }
|
||||
.mailbox-notes-pane.compact .sticky-note-add { min-height:48px; }
|
||||
@media(max-width:1100px) {
|
||||
.workspace-header { grid-template-columns:1fr; gap:10px; }
|
||||
.workspace-zone-actions { padding:0; border:0; }
|
||||
|
|
@ -580,6 +668,8 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
|||
.mailbox-sender,.mailbox-subject { max-width:46%; }
|
||||
.mailbox-preview { flex-basis:100%; max-width:100%; }
|
||||
.mailbox-hover-actions { display:none!important; }
|
||||
.mailbox-row:hover .mailbox-time { display:block; }
|
||||
.mailbox-row:hover .mailbox-time,.mailbox-row.selected .mailbox-time { display:block; }
|
||||
.mailbox-selection-bar { grid-template-columns:1fr; }
|
||||
.mailbox-selection-summarize { justify-self:start; }
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto!important; transition:none!important; } }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { senderDisplayName } from "./senderAvatar";
|
||||
|
||||
const CATEGORY_ORDER = ["Primary", "Updates", "Promotions", "Social", "Finance"];
|
||||
|
||||
export function emailSearchText(email) {
|
||||
|
|
@ -88,6 +90,95 @@ export function attachmentCount(email) {
|
|||
return Number(email.attachment_count || 0);
|
||||
}
|
||||
|
||||
function emailTimestamp(email) {
|
||||
const value = email.received_at || email.sent_at || email.created_at || email.updated_at;
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
function formatRelativeAge(date) {
|
||||
if (!date) return "";
|
||||
const hours = Math.floor((Date.now() - date.getTime()) / 3600000);
|
||||
if (hours < 1) return "<1h";
|
||||
if (hours < 24) return `${hours}h`;
|
||||
return `${Math.floor(hours / 24)}d`;
|
||||
}
|
||||
|
||||
function extractMeetingTime(text) {
|
||||
const match = text.match(/\b(\d{1,2}:\d{2})\s*(am|pm)?/i)
|
||||
|| text.match(/\b(\d{1,2})\s*(am|pm)\b/i);
|
||||
if (!match) return null;
|
||||
return match[0].replace(/\s+/g, "").toUpperCase();
|
||||
}
|
||||
|
||||
function extractDeadline(text) {
|
||||
if (/tomorrow/i.test(text)) return "Tomorrow";
|
||||
const weekday = text.match(/\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i);
|
||||
if (weekday) {
|
||||
return weekday[1].charAt(0).toUpperCase() + weekday[1].slice(1);
|
||||
}
|
||||
if (/today/i.test(text)) return "Today";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function inboxContext(emails) {
|
||||
const stats = inboxIntelligence(emails);
|
||||
|
||||
const needsReply = emails
|
||||
.filter((email) =>
|
||||
/please|reply|respond|confirm|review|attached|need your|waiting|action required/.test(
|
||||
emailSearchText(email),
|
||||
),
|
||||
)
|
||||
.sort((left, right) => {
|
||||
const leftDate = emailTimestamp(left)?.getTime() || 0;
|
||||
const rightDate = emailTimestamp(right)?.getTime() || 0;
|
||||
return rightDate - leftDate;
|
||||
})
|
||||
.slice(0, 4)
|
||||
.map((email) => ({
|
||||
id: email.id,
|
||||
name: senderDisplayName(email),
|
||||
age: formatRelativeAge(emailTimestamp(email)),
|
||||
}));
|
||||
|
||||
const today = [];
|
||||
emails.forEach((email) => {
|
||||
const text = emailSearchText(email);
|
||||
if (!/meet|meeting|call|interview|sync|calendar/.test(text)) return;
|
||||
const time = extractMeetingTime(text);
|
||||
if (!time) return;
|
||||
const label = (email.subject || "Meeting").trim().slice(0, 42);
|
||||
if (!today.some((item) => item.time === time && item.label === label)) {
|
||||
today.push({ time, label });
|
||||
}
|
||||
});
|
||||
|
||||
const deadlines = [];
|
||||
emails.forEach((email) => {
|
||||
const text = emailSearchText(email);
|
||||
if (!/invoice|proposal|payment|due|deadline|bill/.test(text)) return;
|
||||
const due = extractDeadline(text);
|
||||
if (!due) return;
|
||||
const label = (email.subject || "Deadline")
|
||||
.split(/[-–:|]/)
|
||||
.map((part) => part.trim())
|
||||
.find(Boolean)
|
||||
?.slice(0, 28) || "Deadline";
|
||||
if (!deadlines.some((item) => item.label === label && item.due === due)) {
|
||||
deadlines.push({ label, due });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
stats,
|
||||
needsReply,
|
||||
today: today.slice(0, 4),
|
||||
deadlines: deadlines.slice(0, 4),
|
||||
};
|
||||
}
|
||||
|
||||
export function labelCounts(emails) {
|
||||
const counts = {
|
||||
Important: 0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue