refactor: streamline ApprovalCard component across various tools

This commit is contained in:
Anish Sarkar 2026-03-20 20:53:59 +05:30
parent bd2d633546
commit 4bd2071a8d
14 changed files with 225 additions and 267 deletions

View file

@ -129,6 +129,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
@ -175,8 +176,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -190,15 +189,21 @@ function ApprovalCard({
? "Gmail Draft Approved"
: "Create Gmail Draft"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Draft creation was cancelled"
: decided === "edit"
? "Draft creation is in progress with your changes"
: decided === "approve"
? "Draft creation is in progress"
: "Requires your approval to proceed"}
</p>
{decided === "approve" || decided === "edit" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "edit" ? "Draft created with your changes" : "Draft created"}
</p>
) : (
<TextShimmerLoader text={decided === "edit" ? "Creating draft with your changes" : "Creating draft"} size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Draft creation was cancelled"
: "Requires your approval to proceed"}
</p>
)}
</div>
</div>
{!decided && canEdit && (
@ -436,15 +441,7 @@ export const CreateGmailDraftToolUI = makeAssistantToolUI<
CreateGmailDraftResult
>({
toolName: "create_gmail_draft",
render: function CreateGmailDraftUI({ args, result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Creating Gmail draft..." size="sm" />
</div>
);
}
render: function CreateGmailDraftUI({ args, result, status: _status }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -130,6 +130,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
@ -176,8 +177,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -192,15 +191,21 @@ function ApprovalCard({
? "Email Sending Approved"
: "Send Email"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Email sending was cancelled"
: decided === "edit"
? "Email is being sent with your changes"
: decided === "approve"
? "Email is being sent"
: "This will send the email immediately"}
</p>
{decided === "approve" || decided === "edit" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "edit" ? "Email sent with your changes" : "Email sent"}
</p>
) : (
<TextShimmerLoader text={decided === "edit" ? "Sending email with your changes" : "Sending email"} size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Email sending was cancelled"
: "Requires your approval to proceed"}
</p>
)}
</div>
</div>
{!decided && canEdit && (
@ -439,15 +444,7 @@ export const SendGmailEmailToolUI = makeAssistantToolUI<
SendGmailEmailResult
>({
toolName: "send_gmail_email",
render: function SendGmailEmailUI({ args, result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Sending email..." size="sm" />
</div>
);
}
render: function SendGmailEmailUI({ args, result, status: _status }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -149,6 +149,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [deleteFromKb, setDeleteFromKb] = useState(false);
const account = interruptData.context?.account;
@ -180,8 +181,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -196,13 +195,19 @@ function ApprovalCard({
? "Email Trash Approved"
: "Trash Email"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Email trash was cancelled"
: decided === "approve"
? "Email is being trashed"
{decided === "approve" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">Email trashed</p>
) : (
<TextShimmerLoader text="Trashing email" size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Email trash was cancelled"
: "Requires your approval to proceed"}
</p>
</p>
)}
</div>
</div>
</div>
@ -396,15 +401,7 @@ export const TrashGmailEmailToolUI = makeAssistantToolUI<
TrashGmailEmailResult
>({
toolName: "trash_gmail_email",
render: function TrashGmailEmailUI({ result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Trashing email..." size="sm" />
</div>
);
}
render: function TrashGmailEmailUI({ result, status: _status }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -157,6 +157,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
@ -217,8 +218,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
const attendeesList = (args.attendees as string[]) ?? [];
return (
@ -235,15 +234,21 @@ function ApprovalCard({
? "Calendar Event Approved"
: "Create Calendar Event"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Event creation was cancelled"
: decided === "edit"
? "Event creation is in progress with your changes"
: decided === "approve"
? "Event creation is in progress"
: "Requires your approval to proceed"}
</p>
{decided === "approve" || decided === "edit" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "edit" ? "Event created with your changes" : "Event created"}
</p>
) : (
<TextShimmerLoader text={decided === "edit" ? "Creating event with your changes" : "Creating event"} size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Event creation was cancelled"
: "Requires your approval to proceed"}
</p>
)}
</div>
</div>
{!decided && canEdit && (
@ -549,15 +554,7 @@ export const CreateCalendarEventToolUI = makeAssistantToolUI<
CreateCalendarEventResult
>({
toolName: "create_calendar_event",
render: function CreateCalendarEventUI({ args, result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Preparing calendar event..." size="sm" />
</div>
);
}
render: function CreateCalendarEventUI({ args, result }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -177,6 +177,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [deleteFromKb, setDeleteFromKb] = useState(false);
const context = interruptData.context;
@ -209,8 +210,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -225,13 +224,19 @@ function ApprovalCard({
? "Calendar Event Deletion Approved"
: "Delete Calendar Event"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Event deletion was cancelled"
: decided === "approve"
? "Event deletion is in progress"
{decided === "approve" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">Event deleted</p>
) : (
<TextShimmerLoader text="Deleting event" size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Event deletion was cancelled"
: "Requires your approval to proceed"}
</p>
</p>
)}
</div>
</div>
</div>
@ -447,15 +452,7 @@ export const DeleteCalendarEventToolUI = makeAssistantToolUI<
DeleteCalendarEventResult
>({
toolName: "delete_calendar_event",
render: function DeleteCalendarEventUI({ result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Deleting calendar event..." size="sm" />
</div>
);
}
render: function DeleteCalendarEventUI({ result }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -168,6 +168,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
@ -251,8 +252,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -267,15 +266,21 @@ function ApprovalCard({
? "Calendar Event Update Approved"
: "Update Calendar Event"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Event update was cancelled"
: decided === "edit"
? "Event update is in progress with your changes"
: decided === "approve"
? "Event update is in progress"
: "Requires your approval to proceed"}
</p>
{decided === "approve" || decided === "edit" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "edit" ? "Event updated with your changes" : "Event updated"}
</p>
) : (
<TextShimmerLoader text={decided === "edit" ? "Updating event with your changes" : "Updating event"} size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Event update was cancelled"
: "Requires your approval to proceed"}
</p>
)}
</div>
</div>
{!decided && canEdit && (
@ -592,15 +597,7 @@ export const UpdateCalendarEventToolUI = makeAssistantToolUI<
UpdateCalendarEventResult
>({
toolName: "update_calendar_event",
render: function UpdateCalendarEventUI({ result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Looking up calendar event..." size="sm" />
</div>
);
}
render: function UpdateCalendarEventUI({ result }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -134,6 +134,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
@ -202,8 +203,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -216,15 +215,21 @@ function ApprovalCard({
? `${fileTypeLabel} Approved`
: `Create ${fileTypeLabel}`}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "File creation was cancelled"
: decided === "edit"
? "File creation is in progress with your changes"
: decided === "approve"
? "File creation is in progress"
: "Requires your approval to proceed"}
</p>
{decided === "approve" || decided === "edit" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "edit" ? "File created with your changes" : "File created"}
</p>
) : (
<TextShimmerLoader text={decided === "edit" ? "Creating file with your changes" : "Creating file"} size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "File creation was cancelled"
: "Requires your approval to proceed"}
</p>
)}
</div>
{!decided && canEdit && (
<Button
@ -490,15 +495,7 @@ export const CreateGoogleDriveFileToolUI = makeAssistantToolUI<
CreateGoogleDriveFileResult
>({
toolName: "create_google_drive_file",
render: function CreateGoogleDriveFileUI({ args, result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Preparing Google Drive file..." size="sm" />
</div>
);
}
render: function CreateGoogleDriveFileUI({ args, result, status: _status }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -163,6 +163,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [deleteFromKb, setDeleteFromKb] = useState(false);
const account = interruptData.context?.account;
@ -195,8 +196,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -209,13 +208,19 @@ function ApprovalCard({
? "Google Drive File Deletion Approved"
: "Delete Google Drive File"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "File deletion was cancelled"
: decided === "approve"
? "File deletion is in progress"
{decided === "approve" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">File trashed</p>
) : (
<TextShimmerLoader text="Trashing file" size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "File deletion was cancelled"
: "Requires your approval to proceed"}
</p>
</p>
)}
</div>
</div>
@ -420,15 +425,7 @@ export const DeleteGoogleDriveFileToolUI = makeAssistantToolUI<
DeleteGoogleDriveFileResult
>({
toolName: "delete_google_drive_file",
render: function DeleteGoogleDriveFileUI({ result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Looking up file in Google Drive..." size="sm" />
</div>
);
}
render: function DeleteGoogleDriveFileUI({ result, status: _status }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -146,6 +146,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
@ -213,8 +214,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -227,15 +226,21 @@ function ApprovalCard({
? "Linear Issue Approved"
: "Create Linear Issue"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Issue creation was cancelled"
: decided === "edit"
? "Issue creation is in progress with your changes"
: decided === "approve"
? "Issue creation is in progress"
: "Requires your approval to proceed"}
</p>
{decided === "approve" || decided === "edit" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "edit" ? "Issue created with your changes" : "Issue created"}
</p>
) : (
<TextShimmerLoader text={decided === "edit" ? "Creating issue with your changes" : "Creating issue"} size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Issue creation was cancelled"
: "Requires your approval to proceed"}
</p>
)}
</div>
{!decided && canEdit && (
<Button
@ -580,15 +585,7 @@ export const CreateLinearIssueToolUI = makeAssistantToolUI<
CreateLinearIssueResult
>({
toolName: "create_linear_issue",
render: function CreateLinearIssueUI({ args, result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Preparing Linear issue..." size="sm" />
</div>
);
}
render: function CreateLinearIssueUI({ args, result, status: _status }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -135,6 +135,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [deleteFromKb, setDeleteFromKb] = useState(
typeof actionArgs.delete_from_kb === "boolean" ? actionArgs.delete_from_kb : false
);
@ -165,8 +166,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -179,13 +178,19 @@ function ApprovalCard({
? "Linear Issue Deletion Approved"
: "Delete Linear Issue"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Issue deletion was cancelled"
: decided === "approve"
? "Issue deletion is in progress"
{decided === "approve" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
) : (
<TextShimmerLoader text="Deleting issue" size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Issue deletion was cancelled"
: "Requires your approval to proceed"}
</p>
</p>
)}
</div>
</div>
@ -367,15 +372,7 @@ export const DeleteLinearIssueToolUI = makeAssistantToolUI<
DeleteLinearIssueResult
>({
toolName: "delete_linear_issue",
render: function DeleteLinearIssueUI({ result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Preparing Linear issue deletion..." size="sm" />
</div>
);
}
render: function DeleteLinearIssueUI({ result, status: _status }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -186,6 +186,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [editedArgs, setEditedArgs] = useState(initialEditState);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
@ -274,8 +275,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -288,15 +287,21 @@ function ApprovalCard({
? "Linear Issue Update Approved"
: "Update Linear Issue"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Issue update was cancelled"
: decided === "edit"
? "Issue update is in progress with your changes"
: decided === "approve"
? "Issue update is in progress"
: "Requires your approval to proceed"}
</p>
{decided === "approve" || decided === "edit" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "edit" ? "Issue updated with your changes" : "Issue updated"}
</p>
) : (
<TextShimmerLoader text={decided === "edit" ? "Updating issue with your changes" : "Updating issue"} size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Issue update was cancelled"
: "Requires your approval to proceed"}
</p>
)}
</div>
{!decided && canEdit && (
<Button
@ -718,15 +723,7 @@ export const UpdateLinearIssueToolUI = makeAssistantToolUI<
UpdateLinearIssueResult
>({
toolName: "update_linear_issue",
render: function UpdateLinearIssueUI({ result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Preparing Linear issue update..." size="sm" />
</div>
);
}
render: function UpdateLinearIssueUI({ result, status: _status }) {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -118,6 +118,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
@ -178,8 +179,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div
className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300"
@ -194,15 +193,21 @@ function ApprovalCard({
? "Notion Page Approved"
: "Create Notion Page"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Page creation was cancelled"
: decided === "edit"
? "Page creation is in progress with your changes"
: decided === "approve"
? "Page creation is in progress"
: "Requires your approval to proceed"}
</p>
{decided === "approve" || decided === "edit" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "edit" ? "Page created with your changes" : "Page created"}
</p>
) : (
<TextShimmerLoader text={decided === "edit" ? "Creating page with your changes" : "Creating page"} size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Page creation was cancelled"
: "Requires your approval to proceed"}
</p>
)}
</div>
{!decided && canEdit && (
<Button
@ -443,15 +448,7 @@ export const CreateNotionPageToolUI = makeAssistantToolUI<
CreateNotionPageResult
>({
toolName: "create_notion_page",
render: function CreateNotionPageUI({ args, result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Preparing Notion page..." size="sm" />
</div>
);
}
render: function CreateNotionPageUI({ args, result }) {
if (!result) {
return null;
}

View file

@ -139,6 +139,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [deleteFromKb, setDeleteFromKb] = useState(false);
const account = interruptData.context?.account;
@ -170,8 +171,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -184,13 +183,19 @@ function ApprovalCard({
? "Notion Page Deletion Approved"
: "Delete Notion Page"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Page deletion was cancelled"
: decided === "approve"
? "Page deletion is in progress"
{decided === "approve" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">Page deleted</p>
) : (
<TextShimmerLoader text="Deleting page" size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Page deletion was cancelled"
: "Requires your approval to proceed"}
</p>
</p>
)}
</div>
</div>
@ -216,7 +221,7 @@ function ApprovalCard({
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Page to Delete</p>
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
📄 {currentTitle}
{currentTitle}
</div>
</div>
)}
@ -383,15 +388,7 @@ export const DeleteNotionPageToolUI = makeAssistantToolUI<
DeleteNotionPageResult
>({
toolName: "delete_notion_page",
render: function DeleteNotionPageUI({ result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Deleting Notion page..." size="sm" />
</div>
);
}
render: function DeleteNotionPageUI({ result }) {
if (!result) {
return null;
}

View file

@ -120,6 +120,7 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
const wasAlreadyDecided = interruptData.__decided__ != null;
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
@ -157,8 +158,6 @@ function ApprovalCard({
return () => window.removeEventListener("keydown", handler);
}, [handleApprove]);
if (decided && decided !== "reject") return null;
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
@ -171,15 +170,21 @@ function ApprovalCard({
? "Notion Page Update Approved"
: "Update Notion Page"}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Page update was cancelled"
: decided === "edit"
? "Page update is in progress with your changes"
: decided === "approve"
? "Page update is in progress"
: "Requires your approval to proceed"}
</p>
{decided === "approve" || decided === "edit" ? (
wasAlreadyDecided ? (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "edit" ? "Page updated with your changes" : "Page updated"}
</p>
) : (
<TextShimmerLoader text={decided === "edit" ? "Updating page with your changes" : "Updating page"} size="sm" />
)
) : (
<p className="text-xs text-muted-foreground mt-0.5">
{decided === "reject"
? "Page update was cancelled"
: "Requires your approval to proceed"}
</p>
)}
</div>
{!decided && canEdit && (
<Button
@ -238,7 +243,7 @@ function ApprovalCard({
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Current Page</p>
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
📄 {currentTitle}
{currentTitle}
</div>
</div>
)}
@ -389,15 +394,7 @@ export const UpdateNotionPageToolUI = makeAssistantToolUI<
UpdateNotionPageResult
>({
toolName: "update_notion_page",
render: function UpdateNotionPageUI({ args, result, status }) {
if (status.type === "running") {
return (
<div className="my-4 max-w-lg rounded-2xl border bg-muted/30 px-5 py-4 select-none">
<TextShimmerLoader text="Updating Notion page..." size="sm" />
</div>
);
}
render: function UpdateNotionPageUI({ args, result }) {
if (!result) {
return null;
}