Complete React Component Example
Here’s a full implementation of a tool approval interface using React and TypeScript:Copy
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>
);
}