diff --git a/ui/storybook/stories/what-needs-me.stories.tsx b/ui/storybook/stories/what-needs-me.stories.tsx index 2200f90ae66..16f3300d9e0 100644 --- a/ui/storybook/stories/what-needs-me.stories.tsx +++ b/ui/storybook/stories/what-needs-me.stories.tsx @@ -1,11 +1,28 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import { CheckCircle2, Inbox } from "lucide-react"; -import type { AttentionItem, AttentionSourceKind, AttentionSeverity } from "@paperclipai/shared"; +import { ArrowUpDown, CheckCircle2, Inbox, Layers, ListFilter } from "lucide-react"; +import type { AttentionItem, AttentionSourceKind, AttentionSeverity, InboxDismissalKind } from "@paperclipai/shared"; import { AttentionQueueRow } from "@/components/AttentionQueueRow"; +import { IssueGroupHeader } from "@/components/IssueGroupHeader"; +import { Button } from "@/components/ui/button"; +import { + groupAttentionItems, + sortAttentionItems, + type AttentionGroupBy, + type AttentionSortOrder, +} from "@/lib/attention"; const companyId = "company-storybook"; +// Base "now" resolved once at module load so date buckets are stable per render. +const NOW = Date.parse("2026-07-10T12:00:00Z"); +const HOUR = 60 * 60 * 1000; +const DAY = 24 * HOUR; + +function dismissal(kind: InboxDismissalKind, snoozedUntil: string | null): AttentionItem["dismissal"] { + return { kind, dismissedAt: new Date(NOW - HOUR).toISOString(), snoozedUntil, isActive: true }; +} + function item( id: string, sourceKind: AttentionSourceKind, @@ -144,23 +161,98 @@ const POPULATED: AttentionItem[] = [ ), ]; -function Queue({ items }: { items: AttentionItem[] }) { +// Spread activity across recent buckets + attach a couple of projects so the +// date/project group-by modes have something to show. +const ACTIVITY_OFFSETS: Record = { + "recov-1": NOW - 30 * 60 * 1000, + "appr-1": NOW - 2 * HOUR, + "intx-1": NOW - 26 * HOUR, + "review-1": NOW - 27 * HOUR, + "join-1": NOW - 3 * DAY, + "fail-1": NOW - 5 * DAY, + "budget-1": NOW - 40 * DAY, +}; +const PROJECTS: Record = { + "appr-1": { id: "proj-alpha", name: "Alpha", urlKey: "alpha" }, + "intx-1": { id: "proj-alpha", name: "Alpha", urlKey: "alpha" }, + "review-1": { id: "proj-beta", name: "Beta", urlKey: "beta" }, +}; +const POPULATED_DATED: AttentionItem[] = POPULATED.map((it) => ({ + ...it, + activityAt: new Date(ACTIVITY_OFFSETS[it.id] ?? NOW).toISOString(), + project: PROJECTS[it.id] ?? null, +})); + +const SNOOZED: AttentionItem[] = [ + { + ...item("snz-1", "review", "medium", "Design review: settings redesign", "Snoozed until this afternoon."), + activityAt: new Date(NOW - 6 * HOUR).toISOString(), + dismissal: dismissal("snooze", new Date(NOW + 3 * HOUR).toISOString()), + }, + { + ...item("snz-2", "budget_alert", "low", "Budget crossed 70%", "Snoozed until next week.", { inlineResolvable: false }), + activityAt: new Date(NOW - 2 * DAY).toISOString(), + dismissal: dismissal("snooze", new Date(NOW + 5 * DAY).toISOString()), + }, +]; +const DISMISSED: AttentionItem[] = [ + { + ...item("dsm-1", "agent_error_alert", "medium", "Agent error: research analyst", "Dismissed earlier today.", { inlineResolvable: false }), + activityAt: new Date(NOW - 8 * HOUR).toISOString(), + dismissal: dismissal("dismiss", null), + }, +]; + +function ToolbarButton({ icon: Icon, active }: { icon: typeof Layers; active?: boolean }) { + return ( + + ); +} + +function Queue({ + items, + groupBy = "date", + sortOrder = "newest", + snoozed = [], + dismissed = [], + openCurtains = false, +}: { + items: AttentionItem[]; + groupBy?: AttentionGroupBy; + sortOrder?: AttentionSortOrder; + snoozed?: AttentionItem[]; + dismissed?: AttentionItem[]; + openCurtains?: boolean; +}) { const firstInline = items.find((i) => i.inlineResolvable && (i.sourceKind === "approval" || i.sourceKind === "join_request")); const [expandedId, setExpandedId] = useState(firstInline?.id ?? null); const [cleared, setCleared] = useState>(new Set()); const visible = items.filter((i) => !cleared.has(i.id)); + const groups = useMemo( + () => groupAttentionItems(sortAttentionItems(visible, sortOrder), groupBy, { now: NOW }), + [visible, groupBy, sortOrder], + ); + const count = visible.length; + return ( -
-
+
+

What needs me

- {visible.length > 0 && ( - - {visible.length} {visible.length === 1 ? "decision" : "decisions"} - - )} +
+ {count > 0 && ( + + {count} {count === 1 ? "decision" : "decisions"} + + )} + + + +
- {visible.length === 0 ? ( + {count === 0 && snoozed.length === 0 && dismissed.length === 0 ? (
@@ -172,17 +264,74 @@ function Queue({ items }: { items: AttentionItem[] }) {

) : ( -
- {visible.map((it) => ( - setExpandedId((p) => (p === it.id ? null : it.id))} - onDismiss={(dismissed) => setCleared((prev) => new Set(prev).add(dismissed.id))} - /> +
+ {groups.map((group) => ( +
+ {group.items.length}} + /> +
+ {group.items.map((it) => ( + setExpandedId((p) => (p === it.id ? null : it.id))} + onDismiss={(d) => setCleared((prev) => new Set(prev).add(d.id))} + onSnooze={(d) => setCleared((prev) => new Set(prev).add(d.id))} + /> + ))} +
+
))} + + {snoozed.length > 0 && ( +
+ + {openCurtains && ( +
+ {snoozed.map((it) => ( + {}} + onDismiss={() => {}} + onRestore={() => {}} + /> + ))} +
+ )} +
+ )} + + {dismissed.length > 0 && ( +
+ + {openCurtains && ( +
+ {dismissed.map((it) => ( + {}} + onDismiss={() => {}} + onRestore={() => {}} + /> + ))} +
+ )} +
+ )}
)}
@@ -198,8 +347,24 @@ export default meta; type Story = StoryObj; -export const Populated: Story = { - args: { items: POPULATED }, +export const DateGrouping: Story = { + args: { items: POPULATED_DATED, groupBy: "date" }, +}; + +export const GroupedByType: Story = { + args: { items: POPULATED_DATED, groupBy: "type" }, +}; + +export const GroupedByProject: Story = { + args: { items: POPULATED_DATED, groupBy: "project" }, +}; + +export const GroupedBySeverity: Story = { + args: { items: POPULATED_DATED, groupBy: "severity" }, +}; + +export const WithCurtains: Story = { + args: { items: POPULATED_DATED.slice(0, 3), groupBy: "date", snoozed: SNOOZED, dismissed: DISMISSED, openCurtains: true }, }; export const ZeroState: Story = {