Files
patchy631--ai-engineering-hub/open-agent-builder/hooks/useApprovalWatch.ts
T
2026-07-13 12:37:47 +08:00

133 lines
3.7 KiB
TypeScript

import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import { useEffect, useRef } from "react";
/**
* Real-time approval watcher using Convex subscriptions
*
* Why Convex is perfect for human-in-the-loop approvals:
* --------------------------------------------------------
* 1. REAL-TIME: Automatically updates when approval status changes (no polling!)
* 2. REACTIVE: Component re-renders when someone approves/rejects
* 3. PERSISTENT: Survives server restarts (unlike in-memory stores)
* 4. EFFICIENT: Only sends updates when data actually changes
* 5. AUTOMATIC: No need to manually subscribe/unsubscribe
*
* Example workflow:
* -----------------
* 1. Workflow hits "User Approval" node → pauses execution
* 2. Create approval record in Convex
* 3. Frontend watches approval with this hook
* 4. User approves via UI
* 5. Convex instantly notifies all watchers
* 6. Workflow automatically resumes!
*
* Usage:
* ```tsx
* const { status, approval, isApproved, isRejected, isPending } = useApprovalWatch(approvalId);
*
* if (isPending) {
* return <ApprovalButton onApprove={() => approveWorkflow()} />;
* }
*
* if (isApproved) {
* // Workflow will auto-resume!
* return <div>Approved - workflow continuing...</div>;
* }
* ```
*/
export function useApprovalWatch(approvalId: string | null | undefined) {
const prevStatusRef = useRef<string | null>(null);
// Real-time subscription - updates instantly when approval changes!
const result = useQuery(
api.approvals.watchStatus,
approvalId ? { approvalId } : "skip"
);
const status = result?.status || "not_found";
const approval = result?.approval;
const isPending = status === "pending";
const isApproved = status === "approved";
const isRejected = status === "rejected";
const isResolved = isApproved || isRejected;
// Detect status changes for callbacks
useEffect(() => {
if (status && status !== prevStatusRef.current) {
console.log(`Approval ${approvalId} status changed: ${prevStatusRef.current}${status}`);
prevStatusRef.current = status;
}
}, [status, approvalId]);
return {
status,
approval,
isPending,
isApproved,
isRejected,
isResolved,
isLoading: result === undefined,
};
}
/**
* Hook for watching multiple pending approvals for a workflow
*
* Usage:
* ```tsx
* const { pendingApprovals, hasPending } = usePendingApprovals(workflowId);
*
* return (
* <div>
* {hasPending && (
* <div>
* {pendingApprovals.map(approval => (
* <ApprovalCard key={approval.approvalId} approval={approval} />
* ))}
* </div>
* )}
* </div>
* );
* ```
*/
export function usePendingApprovals(workflowId: string | null | undefined) {
const approvals = useQuery(
api.approvals.listPending,
workflowId ? { workflowId: workflowId as any } : "skip"
);
const pendingApprovals = approvals || [];
const hasPending = pendingApprovals.length > 0;
return {
pendingApprovals,
hasPending,
count: pendingApprovals.length,
};
}
/**
* Hook for watching approvals for a specific execution
*
* Useful for showing "Waiting for approval" status in execution panel
*/
export function useExecutionApprovals(executionId: string | null | undefined) {
const approvals = useQuery(
api.approvals.getByExecution,
executionId ? { executionId } : "skip"
);
const allApprovals = approvals || [];
const pendingApprovals = allApprovals.filter(a => a.status === "pending");
const hasPending = pendingApprovals.length > 0;
return {
approvals: allApprovals,
pendingApprovals,
hasPending,
isPaused: hasPending, // Execution is paused if any approvals are pending
};
}