From 2198c7f1f1ed60b36582d47e0634fb7c524d4115 Mon Sep 17 00:00:00 2001
From: Adam Gohain <68021524+akgohain@users.noreply.github.com>
Date: Mon, 13 Apr 2026 16:01:24 -0400
Subject: [PATCH] Add chat proposal cards for new agent actions
---
client/package.json | 3 +-
.../src/__tests__/agentProposalCards.test.js | 87 +++++++++++++++++++
.../src/components/chat/AgentProposalCard.js | 75 ++++++++++++++++
.../contexts/workflow/proposalCardFields.js | 26 ++++++
4 files changed, 190 insertions(+), 1 deletion(-)
create mode 100644 client/src/__tests__/agentProposalCards.test.js
create mode 100644 client/src/components/chat/AgentProposalCard.js
create mode 100644 client/src/contexts/workflow/proposalCardFields.js
diff --git a/client/package.json b/client/package.json
index 7a08018..b533384 100644
--- a/client/package.json
+++ b/client/package.json
@@ -29,7 +29,8 @@
"scripts": {
"start": "react-scripts start",
"build": "cross-env CI=false react-scripts build",
- "electron": "electron ."
+ "electron": "electron .",
+ "test": "react-scripts test"
},
"eslintConfig": {
"extends": [
diff --git a/client/src/__tests__/agentProposalCards.test.js b/client/src/__tests__/agentProposalCards.test.js
new file mode 100644
index 0000000..05b2954
--- /dev/null
+++ b/client/src/__tests__/agentProposalCards.test.js
@@ -0,0 +1,87 @@
+import "@testing-library/jest-dom";
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import AgentProposalCard from "../components/chat/AgentProposalCard";
+
+describe("AgentProposalCard", () => {
+ test("renders prioritize_failure_hotspots card and actions", async () => {
+ const user = userEvent.setup();
+ const onApprove = jest.fn();
+ const onReject = jest.fn();
+ const proposal = {
+ type: "prioritize_failure_hotspots",
+ rationale: "Focus review on areas with recurrent model misses.",
+ payload: {
+ priority_metric: "error_density",
+ max_hotspots: 5,
+ candidate_count: 12,
+ },
+ };
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Prioritize Failure Hotspots")).toBeInTheDocument();
+ expect(screen.getByText(/recurrent model misses/i)).toBeInTheDocument();
+ expect(screen.getByText(/Priority metric:/i)).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Approve" }));
+ await user.click(screen.getByRole("button", { name: "Reject" }));
+
+ expect(onApprove).toHaveBeenCalledWith(proposal);
+ expect(onReject).toHaveBeenCalledWith(proposal);
+ });
+
+ test("renders preview_correction_impact card and actions", async () => {
+ const user = userEvent.setup();
+ const onApprove = jest.fn();
+ const onReject = jest.fn();
+ const proposal = {
+ type: "preview_correction_impact",
+ rationale: "Estimate impact before applying batch corrections.",
+ payload: {
+ target_region: "slice_020-030",
+ estimated_quality_gain: "+4.2% IoU",
+ confidence: "high",
+ },
+ };
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Preview Correction Impact")).toBeInTheDocument();
+ expect(screen.getByText(/Estimate impact/i)).toBeInTheDocument();
+ expect(screen.getByText(/Target region:/i)).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Approve" }));
+ await user.click(screen.getByRole("button", { name: "Reject" }));
+
+ expect(onApprove).toHaveBeenCalledWith(proposal);
+ expect(onReject).toHaveBeenCalledWith(proposal);
+ });
+
+ test("falls back for unknown proposal types", () => {
+ const renderFallback = jest.fn(() =>
Existing card renderer
);
+
+ render(
+ ,
+ );
+
+ expect(renderFallback).toHaveBeenCalled();
+ expect(screen.getByText("Existing card renderer")).toBeInTheDocument();
+ });
+});
diff --git a/client/src/components/chat/AgentProposalCard.js b/client/src/components/chat/AgentProposalCard.js
new file mode 100644
index 0000000..ba09e06
--- /dev/null
+++ b/client/src/components/chat/AgentProposalCard.js
@@ -0,0 +1,75 @@
+import React from "react";
+import {
+ extractKeyFields,
+ PROPOSAL_TITLES,
+} from "../../contexts/workflow/proposalCardFields";
+
+const cardStyle = {
+ border: "1px solid #d9d9d9",
+ borderRadius: 8,
+ padding: 10,
+ marginTop: 8,
+};
+
+const rationaleStyle = {
+ margin: "6px 0 8px",
+ color: "#595959",
+ fontSize: 13,
+};
+
+function ProposalCardBody({ proposal }) {
+ if (
+ proposal?.type !== "prioritize_failure_hotspots" &&
+ proposal?.type !== "preview_correction_impact"
+ ) {
+ return null;
+ }
+
+ const keyFields = extractKeyFields(proposal);
+ const rationale = proposal?.rationale || proposal?.summary || "No rationale provided.";
+
+ return (
+ <>
+ {PROPOSAL_TITLES[proposal.type]}
+ {rationale}
+ {keyFields.length > 0 ? (
+
+ {keyFields.map((field) => (
+ -
+ {field.label}: {String(field.value)}
+
+ ))}
+
+ ) : null}
+ >
+ );
+}
+
+export default function AgentProposalCard({
+ proposal,
+ onApprove,
+ onReject,
+ renderFallback,
+}) {
+ const isNewProposalType =
+ proposal?.type === "prioritize_failure_hotspots" ||
+ proposal?.type === "preview_correction_impact";
+
+ if (!isNewProposalType) {
+ return renderFallback ? renderFallback(proposal) : null;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/contexts/workflow/proposalCardFields.js b/client/src/contexts/workflow/proposalCardFields.js
new file mode 100644
index 0000000..ba56c16
--- /dev/null
+++ b/client/src/contexts/workflow/proposalCardFields.js
@@ -0,0 +1,26 @@
+export const PROPOSAL_TITLES = {
+ prioritize_failure_hotspots: "Prioritize Failure Hotspots",
+ preview_correction_impact: "Preview Correction Impact",
+};
+
+export const PROPOSAL_FIELD_CONFIG = {
+ prioritize_failure_hotspots: [
+ { key: "priority_metric", label: "Priority metric" },
+ { key: "max_hotspots", label: "Hotspot limit" },
+ { key: "candidate_count", label: "Candidates" },
+ ],
+ preview_correction_impact: [
+ { key: "target_region", label: "Target region" },
+ { key: "estimated_quality_gain", label: "Est. quality gain" },
+ { key: "confidence", label: "Confidence" },
+ ],
+};
+
+export const extractKeyFields = (proposal) => {
+ const fieldConfig = PROPOSAL_FIELD_CONFIG[proposal?.type] || [];
+ const source = proposal?.payload || {};
+
+ return fieldConfig
+ .map(({ key, label }) => ({ label, value: source[key] }))
+ .filter(({ value }) => value !== undefined && value !== null && value !== "");
+};