Skip to main content

UX Design Patterns

Batch-Aware Interface Design

Group tools by batch: Always visually group tools that share the same toolExecutionBatchId to help users understand they need to be decided together.
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} toolsMust 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:
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 states
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>
  );
}
Batch-Level Actions: For convenience and abort scenarios
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

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

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

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:
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:
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:
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

I