UX Design Patterns
Batch-Aware Interface Design
Group tools by batch: Always visually group tools that share the sametoolExecutionBatchId
to help users understand they need to be decided together.
Copy
interface BatchGroup {
batchId: string;
tools: ToolApprovalRequest[];
title: string;
description?: string;
}
function ApprovalBatchCard({ batch }: { batch: BatchGroup }) {
return (
<div className="batch-container" data-batch-id={batch.batchId}>
<div className="batch-header">
<h3>{batch.title}</h3>
<span className="batch-indicator">
{batch.tools.length} tools • Must be decided together
</span>
</div>
<div className="tools-list">
{batch.tools.map(tool => (
<ToolApprovalCard key={tool.toolExecutionId} tool={tool} />
))}
</div>
<BatchActions batchId={batch.batchId} />
</div>
);
}
Progress Indicators
Show completion status to guide users toward full batch decisions:Copy
function BatchProgress({ batchId, totalTools, decidedTools }: BatchProgressProps) {
const isComplete = decidedTools === totalTools;
const percentage = (decidedTools / totalTools) * 100;
return (
<div className="batch-progress">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${percentage}%` }}
/>
</div>
<span className={`progress-text ${isComplete ? 'complete' : 'pending'}`}>
{decidedTools} of {totalTools} tools decided
{isComplete && ' • Ready to submit'}
</span>
</div>
);
}
Approval Action Patterns
Individual Tool Actions: For APPROVED and DENIED statesCopy
function ToolActions({ tool, onDecision }: ToolActionsProps) {
return (
<div className="tool-actions">
<button
onClick={() => onDecision(tool.toolExecutionId, 'APPROVED')}
className="approve-button"
>
✓ Approve
</button>
<button
onClick={() => onDecision(tool.toolExecutionId, 'DENIED')}
className="deny-button"
>
✕ Deny
</button>
</div>
);
}
Copy
function BatchActions({ batchId, tools }: BatchActionsProps) {
const [showFeedbackInput, setShowFeedbackInput] = useState(false);
return (
<div className="batch-actions">
{/* Quick actions for approve/deny all */}
<button
onClick={() => approveAllInBatch(batchId, tools)}
className="batch-approve-all"
>
Approve All
</button>
<button
onClick={() => denyAllInBatch(batchId, tools)}
className="batch-deny-all"
>
Deny All
</button>
{/* Feedback input for abort */}
{showFeedbackInput ? (
<FeedbackInput
onSubmit={(feedback) => abortBatchWithFeedback(batchId, tools, feedback)}
onCancel={() => setShowFeedbackInput(false)}
/>
) : (
<button
onClick={() => setShowFeedbackInput(true)}
className="batch-abort"
>
Provide Feedback & Stop
</button>
)}
</div>
);
}
State Management Strategies
Centralized Batch Manager
Copy
class ApprovalStateManager {
private batches = new Map<string, BatchState>();
private listeners = new Set<StateListener>();
addApprovalRequest(request: ToolApprovalRequest) {
const batchId = request.toolExecutionBatchId;
if (!this.batches.has(batchId)) {
this.batches.set(batchId, {
batchId,
tools: [],
decisions: new Map(),
isComplete: false,
canSubmit: false,
submittedAt: null
});
}
this.batches.get(batchId)!.tools.push(request);
this.notifyListeners();
}
setDecision(toolExecutionId: string, decision: ApprovalDecision) {
for (const batch of this.batches.values()) {
const tool = batch.tools.find(t => t.toolExecutionId === toolExecutionId);
if (tool) {
batch.decisions.set(toolExecutionId, decision);
this.updateBatchState(batch);
this.notifyListeners();
break;
}
}
}
private updateBatchState(batch: BatchState) {
const totalTools = batch.tools.length;
const decidedTools = batch.decisions.size;
batch.isComplete = decidedTools === totalTools;
batch.canSubmit = batch.isComplete && this.validateBatchDecisions(batch);
}
private validateBatchDecisions(batch: BatchState): boolean {
const decisions = Array.from(batch.decisions.values());
const hasAbort = decisions.includes('ABORTED');
const hasOtherStates = decisions.some(d => d !== 'ABORTED');
// Invalid: mixed abort with other states
if (hasAbort && hasOtherStates) {
return false;
}
return true;
}
async submitBatch(batchId: string, feedback?: string) {
const batch = this.batches.get(batchId);
if (!batch || !batch.canSubmit) {
throw new Error(`Batch ${batchId} is not ready for submission`);
}
try {
await this.apiSubmitter.submitBatch(batch, feedback);
batch.submittedAt = new Date();
this.notifyListeners();
} catch (error) {
console.error('Batch submission failed:', error);
throw error;
}
}
}
React Hook Integration
Copy
function useApprovalBatch(batchId: string) {
const [batch, setBatch] = useState<BatchState | null>(null);
useEffect(() => {
const listener = (updatedBatch: BatchState) => {
if (updatedBatch.batchId === batchId) {
setBatch({ ...updatedBatch });
}
};
ApprovalStateManager.addListener(listener);
setBatch(ApprovalStateManager.getBatch(batchId));
return () => ApprovalStateManager.removeListener(listener);
}, [batchId]);
const setDecision = useCallback((toolId: string, decision: ApprovalDecision) => {
ApprovalStateManager.setDecision(toolId, decision);
}, []);
const submitBatch = useCallback((feedback?: string) => {
return ApprovalStateManager.submitBatch(batchId, feedback);
}, [batchId]);
return {
batch,
setDecision,
submitBatch
};
}
Feedback Collection Patterns
Contextual Feedback Input
Copy
function FeedbackInput({
tool,
onSubmit,
placeholder = "Why are you stopping this action?"
}: FeedbackInputProps) {
const [feedback, setFeedback] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async () => {
if (!feedback.trim()) {
toast.error('Please provide feedback before aborting');
return;
}
setIsSubmitting(true);
try {
await onSubmit(feedback);
} catch (error) {
toast.error('Failed to submit feedback');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="feedback-input">
<div className="tool-context">
<h4>Stopping: {tool.toolName}</h4>
<p>This will abort all tools in the current batch.</p>
</div>
<textarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder={placeholder}
className="feedback-textarea"
rows={3}
/>
<div className="feedback-actions">
<button
onClick={handleSubmit}
disabled={!feedback.trim() || isSubmitting}
className="submit-feedback"
>
{isSubmitting ? 'Submitting...' : 'Submit Feedback & Stop'}
</button>
<button onClick={onCancel} className="cancel-feedback">
Cancel
</button>
</div>
</div>
);
}
Feedback Templates
Provide common feedback templates to help users:Copy
const FEEDBACK_TEMPLATES = [
{
category: 'Incorrect Parameters',
templates: [
'The email recipient list is incorrect',
'The calendar time is wrong',
'The file path doesn\'t exist'
]
},
{
category: 'Safety Concerns',
templates: [
'This action could delete important data',
'External recipients should not receive this',
'This requires additional approval'
]
},
{
category: 'Misunderstanding',
templates: [
'The agent misunderstood my request',
'This is not what I wanted to accomplish',
'Please clarify the requirements first'
]
}
];
function FeedbackTemplates({ onSelect }: { onSelect: (text: string) => void }) {
return (
<div className="feedback-templates">
<h5>Quick feedback:</h5>
{FEEDBACK_TEMPLATES.map(category => (
<div key={category.category} className="template-category">
<h6>{category.category}</h6>
{category.templates.map(template => (
<button
key={template}
onClick={() => onSelect(template)}
className="template-button"
>
{template}
</button>
))}
</div>
))}
</div>
);
}
Performance Optimization
Debounced Decision Updates
Prevent excessive API calls when users rapidly change decisions:Copy
function useDebouncedDecisions(delay = 300) {
const [pendingDecisions, setPendingDecisions] = useState<Map<string, ApprovalDecision>>(new Map());
const timeoutRef = useRef<NodeJS.Timeout>();
const setDecision = useCallback((toolId: string, decision: ApprovalDecision) => {
setPendingDecisions(prev => new Map(prev).set(toolId, decision));
// Clear existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set new timeout to batch decision updates
timeoutRef.current = setTimeout(() => {
pendingDecisions.forEach((decision, toolId) => {
ApprovalStateManager.setDecision(toolId, decision);
});
setPendingDecisions(new Map());
}, delay);
}, [pendingDecisions, delay]);
return { setDecision };
}
Optimistic UI Updates
Update UI immediately while API call is in progress:Copy
function useOptimisticBatchSubmit() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [optimisticState, setOptimisticState] = useState<'submitting' | 'submitted' | null>(null);
const submitBatch = useCallback(async (batchId: string, feedback?: string) => {
setIsSubmitting(true);
setOptimisticState('submitting');
try {
// Optimistic update
ApprovalStateManager.markBatchAsSubmitted(batchId);
// Actual API call
await ApprovalStateManager.submitBatch(batchId, feedback);
setOptimisticState('submitted');
} catch (error) {
// Revert optimistic update on error
ApprovalStateManager.revertBatchSubmission(batchId);
setOptimisticState(null);
throw error;
} finally {
setIsSubmitting(false);
}
}, []);
return {
submitBatch,
isSubmitting,
isSubmittedOptimistically: optimisticState === 'submitted'
};
}
Next Steps
- Review Error Handling for robust error management
- See Implementation Examples for complete working code
- Learn about performance monitoring and analytics integration