feat(19-02): create InboxList component with filter/sort controls

InboxList joins agents with their latest messages, provides filter
(all/waiting/completed) and sort (newest/oldest) controls, renders
MessageCard instances, and shows empty state when no messages match.
This commit is contained in:
Lukas May
2026-02-04 21:52:01 +01:00
parent 47a2bb38bf
commit bf3521e3dd

View File

@@ -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<FilterValue>("all");
const [sort, setSort] = useState<SortValue>("newest");
// Join agents with their latest message (match message.senderId to agent.id)
const joined = useMemo(() => {
const latestByAgent = new Map<string, Message>();
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 (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">Agent Inbox</h2>
<Badge variant="secondary">{joined.length}</Badge>
</div>
<Button variant="outline" size="sm" onClick={onRefresh}>
<RefreshCw className="mr-1 h-4 w-4" />
Refresh
</Button>
</div>
{/* Filter and Sort controls */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<span className="text-sm text-muted-foreground">Filter:</span>
{filterOptions.map((opt) => (
<Button
key={opt.value}
variant={filter === opt.value ? "default" : "outline"}
size="sm"
className="h-7 px-2 text-xs"
onClick={() => setFilter(opt.value)}
>
{opt.label}
</Button>
))}
</div>
<div className="flex items-center gap-1">
<span className="text-sm text-muted-foreground">Sort:</span>
{sortOptions.map((opt) => (
<Button
key={opt.value}
variant={sort === opt.value ? "default" : "outline"}
size="sm"
className={cn("h-7 px-2 text-xs")}
onClick={() => setSort(opt.value)}
>
{opt.label}
</Button>
))}
</div>
</div>
{/* Message list or empty state */}
{sorted.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 py-16">
<p className="text-lg font-medium text-muted-foreground">
No pending messages
</p>
<p className="text-sm text-muted-foreground">
Agents will appear here when they have questions or status updates
</p>
</div>
) : (
<div className="space-y-2">
{sorted.map((entry) => (
<MessageCard
key={entry.message.id}
agentName={entry.agent.name}
agentStatus={entry.agent.status}
preview={entry.message.content}
timestamp={entry.message.createdAt}
requiresResponse={entry.message.requiresResponse}
isSelected={selectedAgentId === entry.agent.id}
onClick={() => onSelectAgent(entry.agent.id)}
/>
))}
</div>
)}
</div>
);
}