fix(frontend/backend): improve HITL review UX with proper naming and styling

Backend changes:
- Use user-provided name directly as review message (not wrapped in 'Review required for X execution')
- Pass input_data.name from HITL block instead of generic block type name

Frontend changes:
- Show HITL block name in bold in collapse header
- Use HITL name (not node UUID) in auto-approve text
- Default all review groups to collapsed state
- Remove redundant instructions display from individual cards
- Fix lint error: remove unused instructions variable

This ensures HITL blocks show meaningful names like 'User profile data'
instead of UUIDs or generic 'HumanInTheLoopBlock' in the UI.
This commit is contained in:
Zamil Majdy
2026-01-24 09:28:59 -06:00
parent b67164e66e
commit 164a5dc612
4 changed files with 50 additions and 74 deletions

View File

@@ -134,7 +134,7 @@ class HITLReviewHelper:
graph_id=graph_id,
graph_version=graph_version,
input_data=input_data,
message=f"Review required for {block_name} execution",
message=block_name, # Use block_name directly as the message
editable=editable,
)

View File

@@ -121,7 +121,7 @@ class HumanInTheLoopBlock(Block):
graph_exec_id=graph_exec_id,
graph_id=graph_id,
graph_version=graph_version,
block_name=self.name,
block_name=input_data.name, # Use user-provided name instead of block type
editable=input_data.editable,
)

View File

@@ -56,7 +56,6 @@ export function PendingReviewCard({
}: PendingReviewCardProps) {
const extractedData = extractReviewData(review.payload);
const isDataEditable = review.editable;
const instructions = extractedData.instructions || review.instructions;
const [currentData, setCurrentData] = useState(extractedData.data);
// Sync with external data value when auto-approve is toggled
@@ -142,13 +141,6 @@ export function PendingReviewCard({
}
};
// Helper function to get proper field label
const getFieldLabel = (instructions?: string) => {
if (instructions)
return instructions.charAt(0).toUpperCase() + instructions.slice(1);
return "Data to Review";
};
// Helper function to shorten node ID
const getShortenedNodeId = (id: string) => {
if (id.length <= 8) return id;
@@ -165,46 +157,18 @@ export function PendingReviewCard({
</Text>
)}
{/* Show instructions as field label */}
{instructions && (
<div className="space-y-3">
<Text variant="body" className="font-semibold text-gray-900">
{getFieldLabel(instructions)}
</Text>
{isDataEditable && !autoApproveFuture ? (
renderDataInput()
) : (
<div className="rounded-lg border border-gray-200 bg-white p-3">
<Text variant="small" className="text-gray-600">
{JSON.stringify(currentData, null, 2)}
</Text>
</div>
)}
</div>
)}
{/* If no instructions, show data directly */}
{!instructions && (
<div className="space-y-3">
<Text variant="body" className="font-semibold text-gray-900">
Data to Review
{!isDataEditable && (
<span className="ml-2 text-xs text-muted-foreground">
(Read-only)
</span>
)}
</Text>
{isDataEditable && !autoApproveFuture ? (
renderDataInput()
) : (
<div className="rounded-lg border border-gray-200 bg-white p-3">
<Text variant="small" className="text-gray-600">
{JSON.stringify(currentData, null, 2)}
</Text>
</div>
)}
</div>
)}
{/* Show data input/display */}
<div className="space-y-3">
{isDataEditable && !autoApproveFuture ? (
renderDataInput()
) : (
<div className="rounded-lg border border-gray-200 bg-white p-3">
<Text variant="small" className="text-gray-600">
{JSON.stringify(currentData, null, 2)}
</Text>
</div>
)}
</div>
{/* Auto-approve toggle for this review */}
{showAutoApprove && onAutoApproveFutureChange && (

View File

@@ -274,12 +274,16 @@ export function PendingReviewsList({
<div className="space-y-7">
{Object.entries(groupedReviews).map(([nodeId, nodeReviews]) => {
const isCollapsed = collapsedGroups[nodeId];
// Default to collapsed for all reviews
const isCollapsed = collapsedGroups[nodeId] ?? true;
const displayName =
nodeNameMap[nodeId] || nodeReviews[0]?.node_id || "Unknown Block";
const reviewCount = nodeReviews.length;
const shouldShowCollapseHeader =
Object.keys(groupedReviews).length > 1 && reviewCount > 1;
// Get review title from instructions field
// Only show if it's a HITL block (has custom instructions)
const firstReview = nodeReviews[0];
const reviewTitle = firstReview?.instructions;
// Helper to shorten node ID
const getShortenedNodeId = (id: string) => {
@@ -289,30 +293,38 @@ export function PendingReviewsList({
return (
<div key={nodeId} className="space-y-4">
{/* Group Header - Only show if multiple groups AND multiple reviews in this group */}
{shouldShowCollapseHeader && (
<button
onClick={() => toggleGroupCollapse(nodeId)}
className="flex w-full items-center gap-2 text-left"
>
{isCollapsed ? (
<CaretRightIcon size={20} className="text-gray-600" />
) : (
<CaretDownIcon size={20} className="text-gray-600" />
{/* Group Header - Always show for all groups */}
<button
onClick={() => toggleGroupCollapse(nodeId)}
className="flex w-full items-center gap-2 text-left"
>
{isCollapsed ? (
<CaretRightIcon size={20} className="text-gray-600" />
) : (
<CaretDownIcon size={20} className="text-gray-600" />
)}
<div className="flex-1">
{reviewTitle && (
<Text
variant="body"
className="font-semibold text-gray-900"
>
{reviewTitle}
</Text>
)}
<Text variant="body" className="flex-1 text-gray-900">
<Text variant="small" className="text-gray-500">
Node #{getShortenedNodeId(nodeId)}
</Text>
<span className="text-xs text-gray-600">
{reviewCount} {reviewCount === 1 ? "review" : "reviews"}
</span>
</button>
)}
</div>
<span className="text-xs text-gray-600">
{reviewCount} {reviewCount === 1 ? "review" : "reviews"}
</span>
</button>
{/* Reviews in this group */}
{!isCollapsed && (
<div className="space-y-4">
{nodeReviews.map((review, index) => (
{nodeReviews.map((review) => (
<PendingReviewCard
key={review.node_exec_id}
review={review}
@@ -320,7 +332,6 @@ export function PendingReviewsList({
autoApproveFuture={autoApproveFutureMap[nodeId] || false}
externalDataValue={reviewDataMap[review.node_exec_id]}
showAutoApprove={false}
nodeId={shouldShowCollapseHeader ? undefined : nodeId}
/>
))}
@@ -334,14 +345,15 @@ export function PendingReviewsList({
}
/>
<Text variant="small" className="text-gray-700">
Auto-approve future executions of {displayName}
Auto-approve future executions of{" "}
{reviewTitle || `Node #${getShortenedNodeId(nodeId)}`}
</Text>
</div>
{autoApproveFutureMap[nodeId] && (
<Text variant="small" className="pl-11 text-gray-500">
Original data will be used for all {reviewCount}{" "}
{reviewCount === 1 ? "review" : "reviews"} from{" "}
{displayName} in future executions.
{reviewCount === 1 ? "review" : "reviews"} in future
executions.
</Text>
)}
</div>