feat(notes): enable web review decisions

This commit is contained in:
saravanakumardb1 2026-03-10 16:07:13 -07:00
parent db5d7705c1
commit 1bb220b2eb
3 changed files with 103 additions and 2 deletions

View File

@ -4,12 +4,13 @@ import { useEffect, useMemo, useState } from "react";
import { AppShell } from "@/components/AppShell";
import { AgentTimeline } from "@/components/AgentTimeline";
import { ProposalReviewCard } from "@/components/ProposalReviewCard";
import { listAgentTimeline, listApprovalQueue } from "@/lib/review-client";
import { approveReviewItem, listAgentTimeline, listApprovalQueue, rejectReviewItem } from "@/lib/review-client";
import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types";
export default function ReviewsPage() {
const [approvalQueue, setApprovalQueue] = useState<ApprovalQueueItem[]>([]);
const [timeline, setTimeline] = useState<AgentTimelineItem[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
@ -47,6 +48,38 @@ export default function ReviewsPage() {
},
] as const;
async function handleDecision(decision: "approved" | "rejected") {
if (!featuredProposal) {
return;
}
setIsSubmitting(true);
try {
const updated =
decision === "approved"
? await approveReviewItem(featuredProposal)
: await rejectReviewItem(featuredProposal);
setApprovalQueue((current) => current.filter((item) => item.id !== featuredProposal.id));
setTimeline((current) => [
{
id: updated.id,
actor: updated.owner,
action: `human ${decision === "approved" ? "approved" : "rejected"} proposal`,
timestamp: new Date().toISOString(),
status: updated.status,
summary: updated.after ?? updated.title,
},
...current,
]);
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to update review state");
} finally {
setIsSubmitting(false);
}
}
return (
<AppShell
title="Agent review"
@ -103,6 +136,9 @@ export default function ReviewsPage() {
title={featuredProposal.title}
before={featuredProposal.before ?? "No prior summary captured."}
after={featuredProposal.after ?? "No proposed change summary captured yet."}
onApprove={() => void handleDecision("approved")}
onReject={() => void handleDecision("rejected")}
isSubmitting={isSubmitting}
/>
) : null}

View File

@ -2,16 +2,52 @@ export function ProposalReviewCard({
title,
before,
after,
onApprove,
onReject,
isSubmitting = false,
}: {
title: string;
before: string;
after: string;
onApprove?: () => void;
onReject?: () => void;
isSubmitting?: boolean;
}) {
return (
<section className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", alignItems: "center" }}>
<div style={{ fontWeight: 700 }}>{title}</div>
<span className="badge">before / after</span>
<div style={{ display: "flex", gap: "var(--ml-space-2)", alignItems: "center", flexWrap: "wrap" }}>
<span className="badge">before / after</span>
{onReject ? (
<button
type="button"
className="surface-muted"
onClick={onReject}
disabled={isSubmitting}
style={{ border: "1px solid var(--ml-border-subtle)", borderRadius: "var(--ml-radius-md)", padding: "8px 12px" }}
>
Reject
</button>
) : null}
{onApprove ? (
<button
type="button"
onClick={onApprove}
disabled={isSubmitting}
style={{
border: "none",
borderRadius: "var(--ml-radius-md)",
padding: "8px 12px",
background: "var(--ml-accent-primary)",
color: "var(--ml-text-primary)",
fontWeight: 600,
}}
>
Approve
</button>
) : null}
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "var(--ml-space-4)" }}>
<div className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>

View File

@ -77,6 +77,25 @@ function toApprovalQueueItem(action: NoteAgentActionDoc): ApprovalQueueItem {
};
}
async function updateAgentActionState(
id: string,
workspaceId: string,
state: "approved" | "rejected",
): Promise<NoteAgentActionDoc> {
const api = createNotesApiClient();
return api.fetch<NoteAgentActionDoc>(
`/note-agent-actions/${encodeURIComponent(id)}?workspaceId=${encodeURIComponent(workspaceId)}`,
{
method: "PATCH",
body: JSON.stringify({
state,
reviewedAt: new Date().toISOString(),
}),
},
);
}
async function listAgentActionsForWorkspace(workspaceId: string): Promise<NoteAgentActionDoc[]> {
const api = createNotesApiClient();
const response = await api.fetch<NoteAgentActionListResponse>(
@ -110,3 +129,13 @@ export async function listAgentTimeline(noteId?: string): Promise<AgentTimelineI
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
.map(toTimelineItem);
}
export async function approveReviewItem(item: ApprovalQueueItem): Promise<ApprovalQueueItem> {
const updated = await updateAgentActionState(item.id, item.workspaceId, "approved");
return toApprovalQueueItem(updated);
}
export async function rejectReviewItem(item: ApprovalQueueItem): Promise<ApprovalQueueItem> {
const updated = await updateAgentActionState(item.id, item.workspaceId, "rejected");
return toApprovalQueueItem(updated);
}