feat(ui): migrate review workflow primitives

This commit is contained in:
Saravana Achu Mac 2026-05-06 11:43:34 -07:00
parent 1784f72d70
commit c79aa2b6fd
4 changed files with 98 additions and 118 deletions

View File

@ -5,6 +5,17 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { AppShell } from "@/components/AppShell";
import { AgentTimeline } from "@/components/AgentTimeline";
import { ProposalReviewCard } from "@/components/ProposalReviewCard";
import {
Badge,
Button,
ListItemButton,
Panel,
PanelBody,
PanelHeader,
PanelTitle,
StatusBadge,
Textarea,
} from "@/components/ui/Primitives";
import { approveReviewItem, batchReviewItems, listAgentTimeline, listApprovalQueue, rejectReviewItem } from "@/lib/review-client";
import { toast } from "@/lib/toast";
import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types";
@ -175,105 +186,88 @@ export default function ReviewsPage() {
<AppShell
title="Agent review"
description="Approval queue, proposal comparison, and audit-oriented review surfaces for agent-mediated edits."
actions={<div className="badge">Operator workflow shell</div>}
actions={<Badge>Operator workflow shell</Badge>}
>
<section style={{ display: "grid", gridTemplateColumns: "minmax(260px, 320px) minmax(0, 1fr)", gap: "var(--nl-space-4)" }}>
<aside className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
<div style={{ fontWeight: 700 }}>Operator workflows</div>
<Panel as="aside">
<PanelHeader>
<PanelTitle>Operator workflows</PanelTitle>
</PanelHeader>
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
{operatorWorkflows.map((workflow) => (
<Link
key={workflow.id}
href={getWorkflowHref(workflow)}
className="surface-muted"
style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}
style={{ display: "grid", gap: "var(--nl-space-2)" }}
>
<strong>{workflow.name}</strong>
<span style={{ color: "var(--nl-text-secondary)" }}>Owner: {workflow.owner}</span>
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
<span className="badge">{workflow.status}</span>
<span className="badge">Queue: {workflow.queueCount}</span>
<span className="badge">SLA {workflow.sla}</span>
<StatusBadge status={workflow.status === "healthy" ? "approved" : "pending"}>
{workflow.status}
</StatusBadge>
<Badge>Queue: {workflow.queueCount}</Badge>
<Badge>SLA {workflow.sla}</Badge>
</div>
</Link>
))}
</div>
</aside>
</Panel>
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
<div style={{ fontWeight: 700 }}>Approval queue</div>
<Panel>
<PanelHeader>
<PanelTitle>Approval queue</PanelTitle>
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap", alignItems: "center" }}>
{batchMode ? (
<>
<span className="badge">{selectedBatchIds.size} selected</span>
<button type="button" className="badge" onClick={clearBatch} style={{ cursor: "pointer" }}>Clear</button>
<Badge>{selectedBatchIds.size} selected</Badge>
<Button type="button" variant="ghost" size="sm" onClick={clearBatch}>Clear</Button>
</>
) : (
<button type="button" className="badge" onClick={selectAllForBatch} style={{ cursor: "pointer" }}>Select all</button>
<Button type="button" variant="ghost" size="sm" onClick={selectAllForBatch}>Select all</Button>
)}
<span className="badge">status:pending</span>
<StatusBadge status="pending">status:pending</StatusBadge>
</div>
</div>
</PanelHeader>
{error ? <div style={{ color: "var(--nl-text-secondary)" }}>{error}</div> : null}
{batchMode ? (
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
<button
<Button
type="button"
disabled={isSubmitting}
onClick={() => void handleBatchDecision("approved")}
style={{
border: "none",
borderRadius: "var(--nl-radius-md)",
padding: "8px 16px",
background: "var(--nl-accent-primary)",
color: "var(--nl-text-primary)",
fontWeight: 600,
cursor: "pointer",
}}
>
Approve {selectedBatchIds.size}
</button>
<button
</Button>
<Button
type="button"
disabled={isSubmitting}
onClick={() => void handleBatchDecision("rejected")}
className="surface-muted"
style={{
border: "1px solid var(--nl-border-subtle)",
borderRadius: "var(--nl-radius-md)",
padding: "8px 16px",
cursor: "pointer",
}}
variant="destructive"
>
Reject {selectedBatchIds.size}
</button>
</Button>
</div>
) : null}
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
{approvalQueue.map((item) => (
<button
<ListItemButton
key={item.id}
type="button"
className="surface-muted"
selected={
batchMode
? selectedBatchIds.has(item.id)
: featuredProposal?.id === item.id
}
onClick={() => batchMode ? toggleBatchItem(item.id) : setSelectedApprovalId(item.id)}
style={{
padding: "var(--nl-space-4)",
display: "flex",
justifyContent: "space-between",
gap: "var(--nl-space-3)",
alignItems: "center",
flexWrap: "wrap",
textAlign: "left",
width: "100%",
borderColor: batchMode
? selectedBatchIds.has(item.id) ? "var(--nl-accent-primary)" : undefined
: featuredProposal?.id === item.id ? "var(--nl-accent-primary)" : undefined,
background: batchMode
? selectedBatchIds.has(item.id) ? "var(--nl-accent-muted)" : undefined
: featuredProposal?.id === item.id ? "var(--nl-accent-muted)" : undefined,
}}
>
<div style={{ display: "grid", gap: 4 }}>
@ -282,38 +276,31 @@ export default function ReviewsPage() {
</div>
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
{batchMode ? (
<span className="badge">{selectedBatchIds.has(item.id) ? "selected" : "unselected"}</span>
<Badge>{selectedBatchIds.has(item.id) ? "selected" : "unselected"}</Badge>
) : null}
<span className="badge">{item.severity}</span>
<span className="badge">{item.status}</span>
<Badge>{item.severity}</Badge>
<StatusBadge status={item.status === "proposed" ? "proposed" : "pending"}>
{item.status}
</StatusBadge>
</div>
</button>
</ListItemButton>
))}
</div>
</section>
</Panel>
</section>
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
<label htmlFor="review-note" style={{ fontWeight: 600 }}>Review note</label>
<textarea
<Panel>
<PanelBody>
<Textarea
id="review-note"
label="Review note"
value={reviewNote}
onChange={(e) => setReviewNote(e.target.value)}
placeholder="Optional: add a note explaining your decision…"
rows={2}
style={{
width: "100%",
padding: "var(--nl-space-3)",
background: "var(--nl-bg-elevated)",
color: "var(--nl-text-primary)",
border: "1px solid var(--nl-border-subtle)",
borderRadius: "var(--nl-radius-md)",
resize: "vertical",
fontFamily: "inherit",
fontSize: "0.875rem",
}}
/>
</section>
</PanelBody>
</Panel>
{featuredProposal && !batchMode ? (
<ProposalReviewCard

View File

@ -80,8 +80,7 @@ button {
.app-shell {
min-height: 100vh;
display: grid;
grid-template-columns: 280px 1fr;
padding-left: 280px;
}
.sidebar {
@ -95,6 +94,10 @@ button {
padding: var(--nl-space-8);
}
.app-shell > .sidebar {
width: 280px;
}
.surface-card {
border: 1px solid var(--nl-border-default);
border-radius: var(--nl-radius-md);
@ -135,7 +138,7 @@ button {
@media (max-width: 980px) {
.app-shell {
grid-template-columns: 1fr;
padding-left: 0;
}
.sidebar {

View File

@ -1,23 +1,29 @@
import { Panel, PanelHeader, PanelTitle, StatusBadge, Timeline } from "@/components/ui/Primitives";
import type { AgentTimelineItem } from "@/lib/types";
export function AgentTimeline({ items }: { items: AgentTimelineItem[] }) {
return (
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", alignItems: "center" }}>
<div style={{ fontWeight: 700 }}>Agent activity timeline</div>
<span className="badge">review UX</span>
</div>
{items.map((item) => (
<div key={item.id} className="surface-muted" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
<strong>{item.actor}</strong>
<span style={{ color: "var(--nl-text-secondary)" }}>{item.timestamp}</span>
</div>
<div>{item.action}</div>
<div style={{ color: "var(--nl-text-secondary)" }}>{item.summary}</div>
<div className="badge" style={{ width: "fit-content" }}>{item.status}</div>
</div>
))}
</section>
<Panel>
<PanelHeader>
<PanelTitle>Agent activity timeline</PanelTitle>
<StatusBadge status="review">review UX</StatusBadge>
</PanelHeader>
<Timeline
items={items.map((item) => ({
id: item.id,
title: item.actor,
description: (
<span>
{item.action}
<br />
{item.summary}
</span>
),
meta: item.timestamp,
status: item.status,
tone: item.status === "rejected" ? "danger" : item.status === "approved" ? "success" : "info",
}))}
/>
</Panel>
);
}

View File

@ -1,3 +1,5 @@
import { Button, DiffCard, Panel, PanelHeader, PanelTitle, StatusBadge } from "@/components/ui/Primitives";
export function ProposalReviewCard({
title,
before,
@ -14,51 +16,33 @@ export function ProposalReviewCard({
isSubmitting?: boolean;
}) {
return (
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", alignItems: "center" }}>
<div style={{ fontWeight: 700 }}>{title}</div>
<Panel>
<PanelHeader>
<PanelTitle>{title}</PanelTitle>
<div style={{ display: "flex", gap: "var(--nl-space-2)", alignItems: "center", flexWrap: "wrap" }}>
<span className="badge">before / after</span>
<StatusBadge status="review">before / after</StatusBadge>
{onReject ? (
<button
<Button
type="button"
className="surface-muted"
variant="destructive"
onClick={onReject}
disabled={isSubmitting}
style={{ border: "1px solid var(--nl-border-subtle)", borderRadius: "var(--nl-radius-md)", padding: "8px 12px" }}
>
Reject
</button>
</Button>
) : null}
{onApprove ? (
<button
<Button
type="button"
onClick={onApprove}
disabled={isSubmitting}
style={{
border: "none",
borderRadius: "var(--nl-radius-md)",
padding: "8px 12px",
background: "var(--nl-accent-primary)",
color: "var(--nl-text-primary)",
fontWeight: 600,
}}
>
Approve
</button>
</Button>
) : null}
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "var(--nl-space-4)" }}>
<div className="surface-muted" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
<strong>Before</strong>
<div style={{ color: "var(--nl-text-secondary)", whiteSpace: "pre-wrap" }}>{before}</div>
</div>
<div className="surface-muted" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
<strong>After</strong>
<div style={{ color: "var(--nl-text-secondary)", whiteSpace: "pre-wrap" }}>{after}</div>
</div>
</div>
</section>
</PanelHeader>
<DiffCard before={before} after={after} />
</Panel>
);
}