openclaw/extensions/boltbot/dashboard/src/components/ReceiptList.tsx

108 lines
3.6 KiB
TypeScript

import { CheckCircle, XCircle, AlertTriangle } from "lucide-react";
import type { ActionReceipt } from "../types";
import { cn, formatRelativeTime } from "../utils";
import OnboardingCard from "./OnboardingCard";
interface Props {
receipts: ActionReceipt[];
isLoading: boolean;
error: unknown;
onSelect: (r: ActionReceipt) => void;
selectedId: string | null;
hasMore: boolean;
onLoadMore: () => void;
loadingMore?: boolean;
}
const tierBadge: Record<string, string> = {
low: "bg-emerald-500/10 text-emerald-400",
medium: "bg-yellow-500/10 text-yellow-400",
high: "bg-red-500/10 text-red-400",
};
export default function ReceiptList({
receipts,
isLoading,
error,
onSelect,
selectedId,
hasMore,
onLoadMore,
loadingMore = false,
}: Props) {
if (receipts.length === 0) {
return <OnboardingCard isLoading={isLoading} error={error} />;
}
return (
<div role="table" aria-label="Action receipts" aria-busy={isLoading}>
<div role="row" className="grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 px-3 pb-2 text-neutral-400 text-xs uppercase">
<span role="columnheader">Tool</span>
<span role="columnheader">Tier</span>
<span role="columnheader">Time</span>
<span role="columnheader">Status</span>
<span role="columnheader">Anomaly</span>
</div>
<div className="flex flex-col gap-0.5">
{receipts.map((r) => (
<div
key={r.id}
role="row"
tabIndex={0}
aria-selected={selectedId === r.id}
onClick={() => onSelect(r)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelect(r);
}
}}
className={cn(
"grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 items-center px-3 py-2 rounded-lg cursor-pointer transition-colors",
selectedId === r.id
? "bg-neutral-800/50 border-l-2 border-emerald-400"
: "hover:bg-neutral-800/30 border-l-2 border-transparent",
)}
>
<span role="cell" className="text-sm font-mono truncate min-w-0">{r.toolName}</span>
<span role="cell">
<span className={cn("rounded-full px-2 py-0.5 text-xs", tierBadge[r.tier])}>
{r.tier}
</span>
</span>
<span role="cell" className="text-xs text-neutral-400">{formatRelativeTime(r.timestamp)}</span>
<span role="cell">
{r.success ? (
<CheckCircle className="w-4 h-4 text-emerald-400" aria-label="Success" />
) : (
<XCircle className="w-4 h-4 text-red-400" aria-label="Failed" />
)}
</span>
<span role="cell">
{r.anomalies.length > 0 ? (
<AlertTriangle className="w-4 h-4 text-amber-400" aria-label="Has anomalies" />
) : (
<span className="text-neutral-600" aria-label="No anomalies">&mdash;</span>
)}
</span>
</div>
))}
</div>
{hasMore && (
<div className="pt-4 flex justify-center">
<button
onClick={onLoadMore}
disabled={loadingMore}
className={cn(
"px-4 py-2 text-sm bg-neutral-800 rounded-lg transition-colors",
loadingMore ? "opacity-50 cursor-not-allowed" : "hover:bg-neutral-700",
)}
>
{loadingMore ? "Loading…" : "Load More"}
</button>
</div>
)}
</div>
);
}