diff --git a/packages/web/src/components/InboxList.tsx b/packages/web/src/components/InboxList.tsx new file mode 100644 index 0000000..45d6c0f --- /dev/null +++ b/packages/web/src/components/InboxList.tsx @@ -0,0 +1,182 @@ +import { useMemo, useState } from "react"; +import { RefreshCw } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { MessageCard } from "@/components/MessageCard"; +import { cn } from "@/lib/utils"; + +interface Agent { + id: string; + name: string; + status: string; + taskId: string; + updatedAt: string; +} + +interface Message { + id: string; + senderId: string | null; + content: string; + requiresResponse: boolean; + status: string; + createdAt: string; +} + +type FilterValue = "all" | "waiting" | "completed"; +type SortValue = "newest" | "oldest"; + +interface InboxListProps { + agents: Agent[]; + messages: Message[]; + selectedAgentId: string | null; + onSelectAgent: (agentId: string) => void; + onRefresh: () => void; +} + +interface JoinedEntry { + agent: Agent; + message: Message; +} + +export function InboxList({ + agents, + messages, + selectedAgentId, + onSelectAgent, + onRefresh, +}: InboxListProps) { + const [filter, setFilter] = useState("all"); + const [sort, setSort] = useState("newest"); + + // Join agents with their latest message (match message.senderId to agent.id) + const joined = useMemo(() => { + const latestByAgent = new Map(); + + for (const msg of messages) { + if (msg.senderId === null) continue; + const existing = latestByAgent.get(msg.senderId); + if (!existing || new Date(msg.createdAt) > new Date(existing.createdAt)) { + latestByAgent.set(msg.senderId, msg); + } + } + + const entries: JoinedEntry[] = []; + for (const agent of agents) { + const msg = latestByAgent.get(agent.id); + if (msg) { + entries.push({ agent, message: msg }); + } + } + + return entries; + }, [agents, messages]); + + // Filter + const filtered = useMemo(() => { + switch (filter) { + case "waiting": + return joined.filter((e) => e.message.requiresResponse); + case "completed": + return joined.filter((e) => !e.message.requiresResponse); + default: + return joined; + } + }, [joined, filter]); + + // Sort + const sorted = useMemo(() => { + const items = [...filtered]; + items.sort((a, b) => { + const ta = new Date(a.message.createdAt).getTime(); + const tb = new Date(b.message.createdAt).getTime(); + return sort === "newest" ? tb - ta : ta - tb; + }); + return items; + }, [filtered, sort]); + + const filterOptions: { value: FilterValue; label: string }[] = [ + { value: "all", label: "All" }, + { value: "waiting", label: "Waiting" }, + { value: "completed", label: "Completed" }, + ]; + + const sortOptions: { value: SortValue; label: string }[] = [ + { value: "newest", label: "Newest" }, + { value: "oldest", label: "Oldest" }, + ]; + + return ( +
+ {/* Header */} +
+
+

Agent Inbox

+ {joined.length} +
+ +
+ + {/* Filter and Sort controls */} +
+
+ Filter: + {filterOptions.map((opt) => ( + + ))} +
+
+ Sort: + {sortOptions.map((opt) => ( + + ))} +
+
+ + {/* Message list or empty state */} + {sorted.length === 0 ? ( +
+

+ No pending messages +

+

+ Agents will appear here when they have questions or status updates +

+
+ ) : ( +
+ {sorted.map((entry) => ( + onSelectAgent(entry.agent.id)} + /> + ))} +
+ )} +
+ ); +}