feat(ui): add review empty states

This commit is contained in:
Saravana Achu Mac 2026-05-06 13:18:52 -07:00
parent 192a2aafde
commit 6472a58ad1
3 changed files with 62 additions and 9 deletions

View File

@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { AppShell } from "@/components/AppShell"; import { AppShell } from "@/components/AppShell";
import { import {
Badge, Badge,
EmptyState,
LoadingSpinner,
Panel, Panel,
} from "@/components/ui/Primitives"; } from "@/components/ui/Primitives";
import { import {
@ -33,6 +35,7 @@ export default function ReviewsPage() {
const [selectedApprovalId, setSelectedApprovalId] = useState<string | null>(null); const [selectedApprovalId, setSelectedApprovalId] = useState<string | null>(null);
const [selectedBatchIds, setSelectedBatchIds] = useState<Set<string>>(new Set()); const [selectedBatchIds, setSelectedBatchIds] = useState<Set<string>>(new Set());
const [reviewNote, setReviewNote] = useState(""); const [reviewNote, setReviewNote] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -50,6 +53,8 @@ export default function ReviewsPage() {
setTimeline(nextTimeline); setTimeline(nextTimeline);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Unable to load review queue"); setError(err instanceof Error ? err.message : "Unable to load review queue");
} finally {
setIsLoading(false);
} }
})(); })();
}, []); }, []);
@ -281,15 +286,27 @@ export default function ReviewsPage() {
onClear={clearBatch} onClear={clearBatch}
onBatchDecision={(decision) => void handleBatchDecision(decision)} onBatchDecision={(decision) => void handleBatchDecision(decision)}
/> />
{error ? <div style={{ color: "var(--nl-text-secondary)" }}>{error}</div> : null} {isLoading ? (
<LoadingSpinner label="Loading review queue" className="py-8" />
<ReviewQueueList ) : error && approvalQueue.length === 0 ? (
items={approvalQueue} <EmptyState
batchMode={batchMode} title="Unable to load review queue"
selectedBatchIds={selectedBatchIds} description={error}
selectedApprovalId={featuredProposal?.id ?? null} actionLabel="Retry"
onSelectItem={(id) => batchMode ? toggleBatchItem(id) : setSelectedApprovalId(id)} onAction={() => window.location.reload()}
/> />
) : (
<>
{error ? <EmptyState title="Review update failed" description={error} /> : null}
<ReviewQueueList
items={approvalQueue}
batchMode={batchMode}
selectedBatchIds={selectedBatchIds}
selectedApprovalId={featuredProposal?.id ?? null}
onSelectItem={(id) => batchMode ? toggleBatchItem(id) : setSelectedApprovalId(id)}
/>
</>
)}
</Panel> </Panel>
</section> </section>

View File

@ -4,6 +4,7 @@ import { ProposalReviewCard } from "@/components/ProposalReviewCard";
import { import {
Badge, Badge,
Button, Button,
EmptyState,
ListItemButton, ListItemButton,
Panel, Panel,
PanelBody, PanelBody,
@ -126,6 +127,15 @@ export function ReviewQueueList({
selectedApprovalId: string | null; selectedApprovalId: string | null;
onSelectItem: (id: string) => void; onSelectItem: (id: string) => void;
}) { }) {
if (items.length === 0) {
return (
<EmptyState
title="No proposals need review"
description="Agent-mediated changes that need human approval will appear here."
/>
);
}
return ( return (
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}> <div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
{items.map((item) => ( {items.map((item) => (
@ -212,5 +222,19 @@ export function ProposalDiffCard({
} }
export function ReviewTimeline({ items }: { items: AgentTimelineItem[] }) { export function ReviewTimeline({ items }: { items: AgentTimelineItem[] }) {
if (items.length === 0) {
return (
<Panel>
<PanelHeader>
<PanelTitle>Agent activity timeline</PanelTitle>
</PanelHeader>
<EmptyState
title="No review activity yet"
description="Approvals, rejections, and agent changes will be listed as they happen."
/>
</Panel>
);
}
return <AgentTimeline items={items} />; return <AgentTimeline items={items} />;
} }

View File

@ -23,10 +23,12 @@ import {
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyState as BytelystEmptyState,
IconButton as BytelystIconButton, IconButton as BytelystIconButton,
Input as BytelystInput, Input as BytelystInput,
Label as BytelystLabel, Label as BytelystLabel,
ListItemButton as BytelystListItemButton, ListItemButton as BytelystListItemButton,
LoadingSpinner as BytelystLoadingSpinner,
Panel as BytelystPanel, Panel as BytelystPanel,
PanelBody as BytelystPanelBody, PanelBody as BytelystPanelBody,
PanelDescription as BytelystPanelDescription, PanelDescription as BytelystPanelDescription,
@ -57,10 +59,12 @@ import {
type DataListItemProps, type DataListItemProps,
type DataListProps, type DataListProps,
type DiffCardProps, type DiffCardProps,
type EmptyStateProps,
type IconButtonProps, type IconButtonProps,
type InputProps, type InputProps,
type LabelProps, type LabelProps,
type ListItemButtonProps, type ListItemButtonProps,
type LoadingSpinnerProps,
type PanelBodyProps, type PanelBodyProps,
type PanelDescriptionProps, type PanelDescriptionProps,
type PanelHeaderProps, type PanelHeaderProps,
@ -297,3 +301,11 @@ export function DataListItem({ className, ...props }: DataListItemProps) {
export const OperationalList = DataList; export const OperationalList = DataList;
export const OperationalListItem = DataListItem; export const OperationalListItem = DataListItem;
export function EmptyState({ className, ...props }: EmptyStateProps) {
return <BytelystEmptyState className={mergeClassNames("rounded-[var(--nl-radius-md)]", className)} {...props} />;
}
export function LoadingSpinner({ className, ...props }: LoadingSpinnerProps) {
return <BytelystLoadingSpinner className={className} {...props} />;
}