Interactive Answers
Inbox Cleanup
An agent's answer on which newsletters to drop — senders on a volume × opens scatter, an unsubscribe zone you can resize and a checklist to act on.
1 variantMIT
Preview
Inbox cleanup
40 senders · Last 90 days
Plotting 40 senders by volume and opens…
Volume × opens
Last 90 days
0 senders in the zone: 4 or more emails a week, under 10% opened.
Installation
Copies the component into components/ui and installs its dependencies and theme tokens.
pnpm dlx shadcn@latest add https://tuffui.com/r/inbox-cleanup.jsonUsage
Pass your own data and wire the callbacks to your agent runtime.
import { InboxCleanup } from "@/components/ui/inbox-cleanup";
// Rendered by the agent in answer to "which newsletters should I unsubscribe from?"
export function InboxCleanupAnswer({ newsletters }: Props) {
return (
<InboxCleanup
senders={newsletters.map((n) => ({
id: n.id,
name: n.fromName, // "The Daily Brief"
perWeek: n.weeklyVolume, // 7
openRate: n.opened / n.received, // 0.02
}))}
// Four or more emails a week, opened under 10% of the time.
defaultZone={{ minPerWeek: 4, maxOpenRate: 0.1 }}
onUnsubscribe={(ids) => runTool("unsubscribe", { senders: ids })}
onDigest={(ids) => runTool("weekly_digest", { senders: ids })}
onKeep={() => sendFollowUp("Keep them all for now")}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| senders* | InboxSender[] | — | Every sender, in any order. Each is a dot: emails a week across, open rate up a log scale. |
| zone | InboxZone | — | Controlled unsubscribe zone. Senders at or above `minPerWeek` and below `maxOpenRate` are in it. |
| defaultZone | InboxZone | { minPerWeek: 4, maxOpenRate: 0.1 } | Starting zone when uncontrolled. |
| onZoneChange | (zone: InboxZone) => void | — | Called while the zone's left or top edge is dragged, or moved with the arrow keys (Shift for bigger steps). |
| adjustable | boolean | true | Shows the two edge handles. Off fixes the zone where it is. |
| selectedIds | string[] | — | Controlled selection: the senders to unsubscribe from. Ids outside the zone are ignored. |
| defaultSelectedIds | string[] | — | Starting selection when uncontrolled. Omitted, every sender in the zone starts checked, including ones that enter it later. |
| onSelectedChange | (ids: string[]) => void | — | Called when a checkbox is toggled, or a zone dot is clicked or picked with Enter or Space. |
| highlightedId | string | null | — | The sender the agent points at: its dot gets a ring and its tooltip shows until someone hovers another. |
| xDomain | [number, number] | [0, 12] | Emails a week across the x axis. Senders beyond it sit on the edge. |
| xTicks | number[] | [0, 4, 8, 12] | X axis labels. Defaults to four evenly spaced values. |
| yDomain | [number, number] | [0.01, 1] | Open rate, 0–1, up the log y axis. |
| yTicks | number[] | [1, 0.5, 0.25, 0.1] | Y labels and their gridlines. |
| summary | ReactNode | ((stats: InboxCleanupStats) => ReactNode) | — | Agent text above the chart. Defaults to a sentence built from the live zone: senders, threshold and emails a week skipped. |
| status | "analyzing" | "ready" | "ready" | Analyzing plots grey dots only. Ready draws the zone in, turns its senders accent and opens the list and actions. |
| analyzingText | ReactNode | "Plotting 40 senders by volume and opens…" | Shown in place of the summary while analyzing. |
| list | boolean | true | Shows the senders in the zone with checkboxes. Hovering a row picks out its dot, and the other way round. |
| onUnsubscribe | (ids: string[]) => void | — | Shows the primary "Unsubscribe from N" button. Gets the checked ids. |
| onDigest | (ids: string[]) => void | — | Shows "Digest instead". Gets the checked ids. |
| onKeep | () => void | — | Shows "Keep all". |
| icon | Icon | Broom | Phosphor icon in the header tile. |
| title | ReactNode | "Inbox cleanup" | Header title. |
| subtitle | ReactNode | "40 senders · Last 90 days" | Secondary line under the title. |
| chartTitle | string | "Volume × opens" | Chart heading, also its accessible name. |
| period | string | "Last 90 days" | Period label right of the chart heading. |
| zoneLabel | string | "Unsubscribe zone" | Label in the zone's top-right corner. |
| formatPerWeek | (perWeek: number) => string | "7 / wk" | Volume in the tooltip and the list. |
| formatOpenRate | (openRate: number) => string | "2%" | Open rate in the tooltip, the list and the default summary. |
| formatXTick | (value: number, index: number, ticks: number[]) => string | "12 / wk" on the last tick | X axis labels. |
| formatYTick | (value: number) => string | "25%" | Y axis labels. |
| intro | boolean | true | Dots fade and scale in on mount, and the zone's senders turn accent one by one when it appears. Skipped with reduced motion. |
| className | string | — | Extra classes for the root element. |
type InboxSender = {
id: string;
name: string; // "The Daily Brief"
perWeek: number; // emails a week, 7
openRate: number; // share opened, 0–1, 0.02
};
type InboxZone = {
minPerWeek: number; // 4 → at least 4 emails a week
maxOpenRate: number; // 0.1 → opened under 10%
};
type InboxCleanupStats = {
count: number; // senders in the zone
perWeek: number; // emails a week from them
skippedPerWeek: number; // of those, unopened
zone: InboxZone;
};
// Also exported: inZone(sender, zone) => boolean