feat(19-02): create MessageCard component for agent inbox
MessageCard displays agent name with status, message preview (truncated to 80 chars), relative timestamp, and response indicator (filled/empty circle). Uses shadcn Card base with selected state highlighting.
This commit is contained in:
83
packages/web/src/components/MessageCard.tsx
Normal file
83
packages/web/src/components/MessageCard.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function formatRelativeTime(isoDate: string): string {
|
||||||
|
const now = Date.now();
|
||||||
|
const then = new Date(isoDate).getTime();
|
||||||
|
const diffMs = now - then;
|
||||||
|
const diffSec = Math.floor(diffMs / 1000);
|
||||||
|
const diffMin = Math.floor(diffSec / 60);
|
||||||
|
const diffHr = Math.floor(diffMin / 60);
|
||||||
|
const diffDay = Math.floor(diffHr / 24);
|
||||||
|
|
||||||
|
if (diffSec < 60) return "just now";
|
||||||
|
if (diffMin < 60) return `${diffMin} min ago`;
|
||||||
|
if (diffHr < 24) return `${diffHr}h ago`;
|
||||||
|
return `${diffDay}d ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageCardProps {
|
||||||
|
agentName: string;
|
||||||
|
agentStatus: string;
|
||||||
|
preview: string;
|
||||||
|
timestamp: string;
|
||||||
|
requiresResponse: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatStatusLabel(status: string): string {
|
||||||
|
return status.replace(/_/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncatePreview(text: string, maxLength = 80): string {
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return text.slice(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MessageCard({
|
||||||
|
agentName,
|
||||||
|
agentStatus,
|
||||||
|
preview,
|
||||||
|
timestamp,
|
||||||
|
requiresResponse,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
|
}: MessageCardProps) {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer p-4 transition-colors hover:bg-accent/50",
|
||||||
|
isSelected && "bg-accent",
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-sm",
|
||||||
|
requiresResponse ? "text-orange-500" : "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{requiresResponse ? "\u25CF" : "\u25CB"}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-bold">
|
||||||
|
{agentName}{" "}
|
||||||
|
<span className="font-normal text-muted-foreground">
|
||||||
|
({formatStatusLabel(agentStatus)})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 pl-5 text-sm text-muted-foreground">
|
||||||
|
“{truncatePreview(preview)}”
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
|
{formatRelativeTime(timestamp)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user