Skip to main content

Complete React Component Example

Here’s a full implementation of a tool approval interface using React and TypeScript:
import React, { useState, useEffect, useCallback } from 'react';

interface ToolApprovalRequest {
  toolId: string;
  toolName: string;
  toolProvider: string;
  toolCategory: string;
  toolExecutionId: string;
  toolExecutionBatchId: string;
  toolMemoryId: string;
  toolArguments: Record<string, any>;
  approvalResult: 'PENDING_HUMAN_APPROVAL';
}

interface BatchState {
  batchId: string;
  tools: ToolApprovalRequest[];
  decisions: Map<string, ApprovalDecision>;
  isComplete: boolean;
  canSubmit: boolean;
}

type ApprovalDecision = 'APPROVED' | 'DENIED' | 'ABORTED';

export function ToolApprovalInterface({ threadId, apiKey, userId }: {
  threadId: string;
  apiKey: string;
  userId: string;
}) {
  const [batches, setBatches] = useState<Map<string, BatchState>>(new Map());
  const [isSubmitting, setIsSubmitting] = useState<Set<string>>(new Set());
  const [feedback, setFeedback] = useState<Map<string, string>>(new Map());

  // SSE Connection and Event Handling
  useEffect(() => {
    const eventSource = new EventSource(
      `${process.env.REACT_APP_API_BASE_URL}/api/assistants/threads/${threadId}/stream`,
      {
        headers: { 'Authorization': `Bearer ${apiKey}` }
      }
    );

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      
      if (data.type === 'TOOL_EXECUTION_APPROVAL_REQUEST') {
        handleApprovalRequest(data.eventMessage.toolExecutionApprovalRequest);
      }
    };

    eventSource.onerror = (error) => {
      console.error('SSE connection error:', error);
    };

    return () => eventSource.close();
  }, [threadId, apiKey]);

  const handleApprovalRequest = useCallback((requests: ToolApprovalRequest[]) => {
    const batchId = requests[0].toolExecutionBatchId;
    
    setBatches(prev => new Map(prev).set(batchId, {
      batchId,
      tools: requests,
      decisions: new Map(),
      isComplete: false,
      canSubmit: false
    }));
  }, []);

  const setDecision = useCallback((toolExecutionId: string, decision: ApprovalDecision) => {
    setBatches(prev => {
      const newBatches = new Map(prev);
      
      for (const batch of newBatches.values()) {
        if (batch.tools.some(tool => tool.toolExecutionId === toolExecutionId)) {
          const newDecisions = new Map(batch.decisions);
          newDecisions.set(toolExecutionId, decision);
          
          const isComplete = newDecisions.size === batch.tools.length;
          const canSubmit = isComplete && validateBatchDecisions(newDecisions);
          
          newBatches.set(batch.batchId, {
            ...batch,
            decisions: newDecisions,
            isComplete,
            canSubmit
          });
          break;
        }
      }
      
      return newBatches;
    });
  }, []);

  const validateBatchDecisions = (decisions: Map<string, ApprovalDecision>): boolean => {
    const states = Array.from(decisions.values());
    const hasAbort = states.includes('ABORTED');
    const hasOtherStates = states.some(state => state !== 'ABORTED');
    
    return !(hasAbort && hasOtherStates);
  };

  const submitBatch = useCallback(async (batchId: string) => {
    const batch = batches.get(batchId);
    if (!batch || !batch.canSubmit) return;

    setIsSubmitting(prev => new Set(prev).add(batchId));

    try {
      const toolApprovalResults = batch.tools.map(tool => ({
        toolId: tool.toolId,
        toolName: tool.toolName,
        toolProvider: tool.toolProvider,
        toolCategory: tool.toolCategory,
        toolExecutionId: tool.toolExecutionId,
        toolExecutionBatchId: tool.toolExecutionBatchId,
        toolMemoryId: tool.toolMemoryId,
        toolArguments: tool.toolArguments,
        approvalResult: batch.decisions.get(tool.toolExecutionId)
      }));

      const content = [];
      
      // Add feedback if provided
      const batchFeedback = feedback.get(batchId);
      if (batchFeedback) {
        content.push({
          type: 'text',
          text: batchFeedback
        });
      }

      content.push({
        type: 'tool_approval_result',
        tool_approval_results: toolApprovalResults
      });

      const response = await fetch(
        `${process.env.REACT_APP_API_BASE_URL}/api/assistants/threads/${threadId}/messages`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${apiKey}`,
            'Content-Type': 'application/json',
            'X-Envole-User-Id': userId
          },
          body: JSON.stringify({ content })
        }
      );

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || 'Failed to submit approvals');
      }

      // Remove completed batch
      setBatches(prev => {
        const newBatches = new Map(prev);
        newBatches.delete(batchId);
        return newBatches;
      });

      setFeedback(prev => {
        const newFeedback = new Map(prev);
        newFeedback.delete(batchId);
        return newFeedback;
      });

    } catch (error) {
      console.error('Failed to submit batch:', error);
      alert(`Failed to submit approvals: ${error.message}`);
    } finally {
      setIsSubmitting(prev => {
        const newSubmitting = new Set(prev);
        newSubmitting.delete(batchId);
        return newSubmitting;
      });
    }
  }, [batches, feedback, threadId, apiKey, userId]);

  return (
    <div className="tool-approval-interface">
      <h2>Tool Approvals Required</h2>
      
      {Array.from(batches.values()).map(batch => (
        <BatchApprovalCard
          key={batch.batchId}
          batch={batch}
          onSetDecision={setDecision}
          onSubmit={() => submitBatch(batch.batchId)}
          isSubmitting={isSubmitting.has(batch.batchId)}
          feedback={feedback.get(batch.batchId) || ''}
          onFeedbackChange={(text) => setFeedback(prev => new Map(prev).set(batch.batchId, text))}
        />
      ))}
    </div>
  );
}

interface BatchApprovalCardProps {
  batch: BatchState;
  onSetDecision: (toolId: string, decision: ApprovalDecision) => void;
  onSubmit: () => void;
  isSubmitting: boolean;
  feedback: string;
  onFeedbackChange: (feedback: string) => void;
}

function BatchApprovalCard({
  batch,
  onSetDecision,
  onSubmit,
  isSubmitting,
  feedback,
  onFeedbackChange
}: BatchApprovalCardProps) {
  const [showFeedbackInput, setShowFeedbackInput] = useState(false);
  
  const decidedCount = batch.decisions.size;
  const totalCount = batch.tools.length;
  const hasAbortDecision = Array.from(batch.decisions.values()).includes('ABORTED');

  const handleAbortAll = () => {
    batch.tools.forEach(tool => {
      onSetDecision(tool.toolExecutionId, 'ABORTED');
    });
    setShowFeedbackInput(true);
  };

  const handleApproveAll = () => {
    batch.tools.forEach(tool => {
      onSetDecision(tool.toolExecutionId, 'APPROVED');
    });
  };

  const handleDenyAll = () => {
    batch.tools.forEach(tool => {
      onSetDecision(tool.toolExecutionId, 'DENIED');
    });
  };

  return (
    <div className="batch-card">
      <div className="batch-header">
        <h3>Batch {batch.batchId}</h3>
        <div className="batch-progress">
          {decidedCount} of {totalCount} tools decided
        </div>
      </div>

      <div className="tools-list">
        {batch.tools.map(tool => (
          <ToolCard
            key={tool.toolExecutionId}
            tool={tool}
            decision={batch.decisions.get(tool.toolExecutionId)}
            onSetDecision={onSetDecision}
            disabled={hasAbortDecision}
          />
        ))}
      </div>

      <div className="batch-actions">
        <div className="quick-actions">
          <button 
            onClick={handleApproveAll}
            disabled={hasAbortDecision || isSubmitting}
            className="approve-all-btn"
          >
            Approve All
          </button>
          <button 
            onClick={handleDenyAll}
            disabled={hasAbortDecision || isSubmitting}
            className="deny-all-btn"
          >
            Deny All
          </button>
          <button 
            onClick={handleAbortAll}
            disabled={isSubmitting}
            className="abort-all-btn"
          >
            Stop with Feedback
          </button>
        </div>

        {showFeedbackInput && (
          <div className="feedback-section">
            <textarea
              value={feedback}
              onChange={(e) => onFeedbackChange(e.target.value)}
              placeholder="Explain why you're stopping these tools..."
              className="feedback-textarea"
              rows={3}
            />
          </div>
        )}

        <button
          onClick={onSubmit}
          disabled={!batch.canSubmit || isSubmitting}
          className="submit-batch-btn"
        >
          {isSubmitting ? 'Submitting...' : 'Submit Decisions'}
        </button>
      </div>
    </div>
  );
}

interface ToolCardProps {
  tool: ToolApprovalRequest;
  decision?: ApprovalDecision;
  onSetDecision: (toolId: string, decision: ApprovalDecision) => void;
  disabled: boolean;
}

function ToolCard({ tool, decision, onSetDecision, disabled }: ToolCardProps) {
  return (
    <div className={`tool-card ${decision ? `decided-${decision.toLowerCase()}` : 'undecided'}`}>
      <div className="tool-header">
        <h4>{tool.toolName}</h4>
        <span className="tool-provider">{tool.toolProvider}</span>
      </div>

      <div className="tool-arguments">
        <h5>Arguments:</h5>
        <pre>{JSON.stringify(tool.toolArguments, null, 2)}</pre>
      </div>

      <div className="tool-actions">
        <button
          onClick={() => onSetDecision(tool.toolExecutionId, 'APPROVED')}
          disabled={disabled}
          className={`action-btn approve-btn ${decision === 'APPROVED' ? 'active' : ''}`}
        >
Approve
        </button>
        <button
          onClick={() => onSetDecision(tool.toolExecutionId, 'DENIED')}
          disabled={disabled}
          className={`action-btn deny-btn ${decision === 'DENIED' ? 'active' : ''}`}
        >
Deny
        </button>
      </div>

      {decision && (
        <div className="decision-indicator">
          Decision: <strong>{decision}</strong>
        </div>
      )}
    </div>
  );
}
This example provides a complete, working implementations that you can adapt to your specific application architecture and requirements. The key patterns demonstrated include proper batch management, state validation, error handling, and API integration following the documented constraints and best practices.
I