From 53922952c6002095ee8d345a9d20d287276f41fb Mon Sep 17 00:00:00 2001 From: debu-2 Date: Sat, 3 Oct 2026 22:34:34 +0530 Subject: [PATCH] UI enhancement --- frontend/src/components/AgAgentPanel.jsx | 176 ++++++++----- frontend/src/components/AgentInsight.jsx | 48 ++++ frontend/src/components/AgentSpark.jsx | 18 ++ frontend/src/components/CommandPalette.jsx | 128 +++++++++ frontend/src/components/ComposeModal.jsx | 10 +- frontend/src/components/MailDetail.jsx | 33 ++- frontend/src/components/MailRow.jsx | 48 +++- .../src/components/MailboxSelectionBar.jsx | 56 ++++ frontend/src/components/MailboxView.jsx | 226 ++++++++++++++-- frontend/src/components/StickyNotesPanel.jsx | 245 +++++++++++------- frontend/src/hooks/useAgentInsight.js | 51 ++++ frontend/src/styles.css | 206 ++++++++++----- frontend/src/utils/inboxSignals.js | 91 +++++++ 13 files changed, 1076 insertions(+), 260 deletions(-) create mode 100644 frontend/src/components/AgentInsight.jsx create mode 100644 frontend/src/components/AgentSpark.jsx create mode 100644 frontend/src/components/CommandPalette.jsx create mode 100644 frontend/src/components/MailboxSelectionBar.jsx create mode 100644 frontend/src/hooks/useAgentInsight.js diff --git a/frontend/src/components/AgAgentPanel.jsx b/frontend/src/components/AgAgentPanel.jsx index fca32f5..2aea570 100644 --- a/frontend/src/components/AgAgentPanel.jsx +++ b/frontend/src/components/AgAgentPanel.jsx @@ -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 (
- +

AG Agent

Your email co-pilot

@@ -62,49 +50,113 @@ export default function AgAgentPanel({ account, emails, focus, onQuickAction })
{greeting(firstName)} - Here's what needs your attention. - {focus &&

{focus}

} + {!insight && ( + hasSignals ? ( + Here's what needs your attention. + ) : ( + You're all caught up. No urgent messages or outstanding actions. + ) + )} + {!insight && focus &&

{focus}

}
-
- {cards.map((card) => { - const Icon = card.icon; - const count = stats[card.key] || 0; - return ( - + ); + })} +
+ ) : !insight && ( +
+ +

Inbox is quiet right now.

+ {stats.unread > 0 && ( + - ); - })} -
+ )} +
+ )}

Quick actions

- -
+ + {!insight && today.length > 0 && ( +
+

Today

+
    + {today.map((item) => ( +
  • + + {item.label} +
  • + ))} +
+
+ )} + + {!insight && needsReply.length > 0 && ( +
+

Needs reply

+
    + {needsReply.map((item) => ( +
  • + +
  • + ))} +
+
+ )} + + {!insight && deadlines.length > 0 && ( +
+

Deadlines

+
    + {deadlines.map((item) => ( +
  • + {item.label} + {item.due} +
  • + ))} +
+
+ )}
); } diff --git a/frontend/src/components/AgentInsight.jsx b/frontend/src/components/AgentInsight.jsx new file mode 100644 index 0000000..a568b3b --- /dev/null +++ b/frontend/src/components/AgentInsight.jsx @@ -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 ( +
+ {insight.processing && insight.phase === "understanding" && ( +

+ Understanding conversation… +

+ )} + + {showSummary && ( +
+

Summary

+

{insight.summary}

+
+ )} + + {showDetails && ( +
+

Extracted details

+
    + {insight.details.map((item) => ( +
  • {item}
  • + ))} +
+
+ )} + + {showActions && ( +
+

Suggested actions

+
    + {insight.actions.map((item) => ( +
  • {item}
  • + ))} +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/AgentSpark.jsx b/frontend/src/components/AgentSpark.jsx new file mode 100644 index 0000000..b04b5d6 --- /dev/null +++ b/frontend/src/components/AgentSpark.jsx @@ -0,0 +1,18 @@ +export default function AgentSpark({ pulse = false, className = "" }) { + return ( + + ); +} + +export function AgentLabel({ children, className = "" }) { + return ( + + {children} + + ); +} diff --git a/frontend/src/components/CommandPalette.jsx b/frontend/src/components/CommandPalette.jsx new file mode 100644 index 0000000..b62a9b3 --- /dev/null +++ b/frontend/src/components/CommandPalette.jsx @@ -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 ( +
+
event.stopPropagation()} + > + +
    + {filtered.map((command, index) => { + const Icon = command.icon; + return ( +
  • + +
  • + ); + })} + {filtered.length === 0 && ( +
  • No matching commands
  • + )} +
+
+ ↑↓ navigate + ↵ run + / or Ctrl K open +
+
+
+ ); +} diff --git a/frontend/src/components/ComposeModal.jsx b/frontend/src/components/ComposeModal.jsx index 162213b..bb36db3 100644 --- a/frontend/src/components/ComposeModal.jsx +++ b/frontend/src/components/ComposeModal.jsx @@ -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 }) {
- +

New message

From {account.email}
diff --git a/frontend/src/components/MailDetail.jsx b/frontend/src/components/MailDetail.jsx index c5dcc92..ebdfb28 100644 --- a/frontend/src/components/MailDetail.jsx +++ b/frontend/src/components/MailDetail.jsx @@ -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 (
{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 ? : } + {email.is_read ? : } )} {email && mailbox === "trash" && ( )} {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"} > - + )}
@@ -87,6 +97,19 @@ export default function MailDetail({ {error &&
{error}
} {email && !loading && (
+
+ + + {files > 0 && ( + + )} +

{email.subject?.trim() || "(No subject)"}

diff --git a/frontend/src/components/MailRow.jsx b/frontend/src/components/MailRow.jsx index 185c201..08d12b7 100644 --- a/frontend/src/components/MailRow.jsx +++ b/frontend/src/components/MailRow.jsx @@ -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 (
onOpen(email)} onKeyDown={handleKeyDown} aria-label={`${unread ? "Unread" : "Read"} email from ${senderName}: ${subject}`} + aria-selected={selected} > - +
@@ -109,7 +120,7 @@ export default function MailRow({ ))} {files > 0 && ( - + {files > 1 ? files : ""} )} @@ -117,12 +128,29 @@ export default function MailRow({ )}
-
- - - - - +
+ + + + + + + +
); diff --git a/frontend/src/components/MailboxSelectionBar.jsx b/frontend/src/components/MailboxSelectionBar.jsx new file mode 100644 index 0000000..d3dffaf --- /dev/null +++ b/frontend/src/components/MailboxSelectionBar.jsx @@ -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 ( +
+
+ + {count} selected +
+
+ + + + + + + + +
+ +
+ ); +} diff --git a/frontend/src/components/MailboxView.jsx b/frontend/src/components/MailboxView.jsx index 5a8db3d..b46d91a 100644 --- a/frontend/src/components/MailboxView.jsx +++ b/frontend/src/components/MailboxView.jsx @@ -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…" /> - Ctrl K +
@@ -379,8 +532,8 @@ export default function MailboxView({ > -
@@ -403,7 +556,24 @@ export default function MailboxView({
- {mailbox === "inbox" && ( + {selectedCount > 0 && ( + 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 && (
{CATEGORY_TABS.map(({ name, icon: Icon }) => (
@@ -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); + }} /> - +
+ + setCommandOpen(false)} + onCommand={handleCommand} + />
); } diff --git a/frontend/src/components/StickyNotesPanel.jsx b/frontend/src/components/StickyNotesPanel.jsx index 30e2877..ee16b9c 100644 --- a/frontend/src/components/StickyNotesPanel.jsx +++ b/frontend/src/components/StickyNotesPanel.jsx @@ -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 (
@@ -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,107 +201,130 @@ export default function StickyNotesPanel({ compact = false }) { }); } + const contextLabel = contextEmail ? senderDisplayName(contextEmail) : null; + return ( -