From 9a913147fb9ecf6b86f103465a560ff9ebd98fa1 Mon Sep 17 00:00:00 2001
From: Khalid Abdi
Date: Thu, 18 Jun 2026 19:51:24 +0300
Subject: [PATCH] frontend: make AI proposal cards editable and de-densify them
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The single proposal card crammed every proposed record into comma-joined
summary lines — an inventory import became an unreadable wall of text.
Render inventory/invoice records as a compact scrollable item list, and
add an Edit button (single card + per-row in the batch review) that opens
a per-kind edit dialog so the clinician can adjust the data — whole record
or individual items — before it is committed.
New RecordEditDialog is schema-driven (EDIT_SCHEMAS) per action kind,
including add/remove rows for invoice line items and inventory items.
Co-Authored-By: Claude Opus 4.8
---
.../components/chat/action-preview-card.tsx | 147 ++++++++-
.../chat/batch-action-preview-card.tsx | 49 ++-
.../components/chat/record-edit-dialog.tsx | 295 ++++++++++++++++++
frontend/lib/i18n/locales/en/translation.json | 35 +++
4 files changed, 507 insertions(+), 19 deletions(-)
create mode 100644 frontend/components/chat/record-edit-dialog.tsx
diff --git a/frontend/components/chat/action-preview-card.tsx b/frontend/components/chat/action-preview-card.tsx
index 100ca68..e3c8658 100644
--- a/frontend/components/chat/action-preview-card.tsx
+++ b/frontend/components/chat/action-preview-card.tsx
@@ -6,6 +6,7 @@ import {
CalendarPlus,
Check,
ClipboardList,
+ Pencil,
Pill,
Receipt,
X,
@@ -13,9 +14,10 @@ import {
import { useState } from "react";
import { useTranslation } from "react-i18next";
+import { RecordEditDialog } from "@/components/chat/record-edit-dialog";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
-import type { ActionPreviewData } from "@/lib/ai-chat";
+import type { ActionPreviewData, ActionPreviewKind } from "@/lib/ai-chat";
import { type AppointmentInput, createAppointment } from "@/lib/appointments";
import {
createInvoice,
@@ -110,20 +112,123 @@ export async function commitAction(data: ActionPreviewData): Promise {
}
}
+// A structured, de-densified view of a proposed record. Inventory and invoice
+// records carry an item array — rendered as a compact scrollable list (one row
+// per item) rather than a single comma-joined wall of text; everything else
+// uses the per-kind summary lines.
+export function RecordSummary({
+ kind,
+ record,
+}: {
+ kind: ActionPreviewKind;
+ record: Record;
+}) {
+ const { t } = useTranslation();
+
+ if (kind === "inventory") {
+ const items = (record.items as InventoryInput[] | undefined) ?? [];
+ return (
+
+ );
+ }
+
+ const lines = summarize({ kind, record } as ActionPreviewData);
+ return (
+
+ {lines.map((line, i) => (
+
+ {line}
+
+ ))}
+
+ );
+}
+
// The human approval gate for an agent-proposed add. The agent drafts the record
-// (dry run, nothing written); the clinician reviews it here and must approve
-// before it is committed via the matching RBAC-gated create endpoint.
+// (dry run, nothing written); the clinician reviews it here, may edit it, and
+// must approve before it is committed via the matching RBAC-gated create endpoint.
export function ActionPreviewCard({ data }: { data: ActionPreviewData }) {
const { t } = useTranslation();
const [status, setStatus] = useState("pending");
+ // Editable working copy of the proposed record (edits commit, not the draft).
+ const [record, setRecord] = useState>(
+ data.record as Record,
+ );
+ const [editOpen, setEditOpen] = useState(false);
const Icon = ICONS[data.kind];
const hasIssues = (data.issues?.length ?? 0) > 0;
- const lines = summarize(data);
const approve = async () => {
setStatus("committing");
try {
- await commitAction(data);
+ await commitAction({ ...data, record });
setStatus("done");
notify.success(
t("chat.actionCard.addedTitle"),
@@ -138,6 +243,8 @@ export function ActionPreviewCard({ data }: { data: ActionPreviewData }) {
}
};
+ const editable = status === "pending";
+
return (