"use client"; import { motion, AnimatePresence } from "framer-motion"; import { useState, useEffect, useCallback } from "react"; import { Workflow, WorkflowExecution, NodeExecutionResult, WorkflowPendingAuth } from "@/lib/workflow/types"; import { toast } from "sonner"; import { Bot, GitBranch, Repeat, CheckCircle, Braces, Search, Shield, Plug, Play, StopCircle, Zap, FileText, } from "lucide-react"; import Button from "@/components/shared/button/Button"; interface ExecutionPanelProps { workflow: Workflow | null; execution: WorkflowExecution | null; nodeResults: Record; isRunning: boolean; currentNodeId: string | null; onRun: (input: string) => void; onResumePendingAuth: () => Promise; onClose: () => void; environment: 'draft' | 'production'; pendingAuth: WorkflowPendingAuth | null; } const getNodeIcon = (nodeType: string) => { const iconMap: Record = { 'agent': Bot, 'mcp': Plug, 'firecrawl': Zap, 'if-else': GitBranch, 'while': Repeat, 'user-approval': CheckCircle, 'transform': Braces, 'file-search': Search, 'extract': FileText, 'end': StopCircle, 'start': Play, }; return iconMap[nodeType] || Plug; }; const getNodeColor = (nodeType: string) => { const colorMap: Record = { 'agent': 'bg-heat-40', 'mcp': 'bg-teal-500', 'firecrawl': 'bg-black-alpha-12', 'if-else': 'bg-amber-500', 'while': 'bg-cyan-500', 'user-approval': 'bg-gray-400', 'transform': 'bg-violet-500', 'file-search': 'bg-indigo-500', 'extract': 'bg-purple-500', 'end': 'bg-gray-500', 'start': 'bg-gray-600', }; return colorMap[nodeType] || 'bg-gray-500'; }; export default function ExecutionPanel({ workflow, execution, nodeResults, isRunning, currentNodeId, onRun, onResumePendingAuth, onClose, environment, pendingAuth, }: ExecutionPanelProps) { // Track Google Doc creation for toast notifications const [notifiedDocs, setNotifiedDocs] = useState>(new Set()); const [isResumingAuth, setIsResumingAuth] = useState(false); const [copiedAuthLink, setCopiedAuthLink] = useState(false); const handleCopyAuthLink = useCallback(() => { if (!pendingAuth?.authUrl) return; navigator.clipboard.writeText(pendingAuth.authUrl); setCopiedAuthLink(true); setTimeout(() => setCopiedAuthLink(false), 2000); toast.success('Authorization link copied'); }, [pendingAuth?.authUrl]); const handleResumeAuthorization = useCallback(async () => { if (isResumingAuth) return; setIsResumingAuth(true); try { await onResumePendingAuth(); toast.success('Resuming workflow...'); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to resume workflow'; toast.error(message); } finally { setIsResumingAuth(false); } }, [isResumingAuth, onResumePendingAuth]); // Check for Google Doc creation and show toast useEffect(() => { Object.entries(nodeResults).forEach(([nodeId, result]) => { if (result.status === 'completed' && result.output && !notifiedDocs.has(nodeId)) { const output = result.output; const outputStr = typeof output === 'string' ? output : JSON.stringify(output, null, 2); const outputObj = typeof output === 'object' ? output : null; // Check if this is a Google Docs creation result const isGoogleDocResult = outputObj?.result?.output?.value?.documentUrl || outputStr.includes('documentUrl') || outputStr.includes('Executive Summary'); if (isGoogleDocResult) { let docUrl: string | null = null; let docTitle: string | null = null; // Extract document URL and title from the result if (outputObj?.result?.output?.value?.documentUrl) { docUrl = outputObj.result.output.value.documentUrl; docTitle = outputObj.result.output.value.title || 'Executive Summary'; } else if (outputStr.includes('documentUrl')) { try { const parsed = JSON.parse(outputStr); docUrl = parsed.result?.output?.value?.documentUrl || parsed.documentUrl; docTitle = parsed.result?.output?.value?.title || parsed.documentTitle || 'Executive Summary'; } catch (e) { // Fallback: try to extract URL from string const urlMatch = outputStr.match(/https:\/\/docs\.google\.com\/document\/d\/[^\/\s]+/); if (urlMatch) { docUrl = urlMatch[0]; docTitle = 'Executive Summary'; } } } if (docUrl) { // Show success toast toast.success('Document Updated', { description: `Your executive summary "${docTitle}" has been created in Google Docs.`, action: { label: 'Open Document', onClick: () => window.open(docUrl, '_blank'), }, duration: 8000, // Show for 8 seconds }); // Mark this node as notified setNotifiedDocs(prev => new Set(prev).add(nodeId)); } } } }); }, [nodeResults, notifiedDocs]); useEffect(() => { setCopiedAuthLink(false); }, [pendingAuth?.authId]); // Get input variables from Start node const startNode = workflow?.nodes.find(n => (n.data as any)?.nodeType === 'start'); const inputVariables = (startNode?.data as any)?.inputVariables || []; console.log('ExecutionPanel - Start node:', startNode); console.log('ExecutionPanel - Input variables:', inputVariables); // Initialize input state from variables const [inputValues, setInputValues] = useState>({}); // Update inputValues when inputVariables change useEffect(() => { if (inputVariables.length > 0) { const defaults = inputVariables.reduce((acc: any, v: any) => { acc[v.name] = v.defaultValue || (v.type === 'url' ? 'https://firecrawl.dev' : ''); return acc; }, {}); setInputValues(defaults); console.log('Set input values from variables:', defaults); } else { setInputValues({ input: 'https://firecrawl.dev' }); console.log('Set default input value'); } }, [workflow?.id, inputVariables.length]); // Re-initialize when workflow or variables change const [showInput, setShowInput] = useState(true); const [copiedError, setCopiedError] = useState(null); const [copiedAll, setCopiedAll] = useState(false); const [expandedSchemas, setExpandedSchemas] = useState>(new Set()); const handleCopyAllResults = () => { // Collect all results from the workflow with data flow information const results = { environment, workflowId: workflow?.id, executionId: execution?.id, status: execution?.status, inputs: inputValues, dataFlow: { nodes: Object.entries(nodeResults).map(([nodeId, result]) => { const node = workflow?.nodes.find(n => n.id === nodeId); const nodeName = (node?.data as any)?.nodeName || (typeof node?.data?.label === 'string' ? node.data.label : nodeId); // Find incoming connections (edges where this node is the target) const incomingEdges = workflow?.edges.filter(e => e.target === nodeId) || []; const incomingConnections = incomingEdges.map(edge => { const sourceNode = workflow?.nodes.find(n => n.id === edge.source); const sourceNodeName = (sourceNode?.data as any)?.nodeName || edge.source; return { fromNodeId: edge.source, fromNodeName: sourceNodeName, edgeLabel: edge.label || null, sourceOutput: nodeResults[edge.source]?.output || null, }; }); // Find outgoing connections (edges where this node is the source) const outgoingEdges = workflow?.edges.filter(e => e.source === nodeId) || []; const outgoingConnections = outgoingEdges.map(edge => { const targetNode = workflow?.nodes.find(n => n.id === edge.target); const targetNodeName = (targetNode?.data as any)?.nodeName || edge.target; return { toNodeId: edge.target, toNodeName: targetNodeName, edgeLabel: edge.label || null, receivedInput: result.output || null, }; }); return { nodeId, nodeName, nodeType: (node?.data as any)?.nodeType || node?.type, status: result.status, incomingConnections, output: result.output, toolCalls: result.toolCalls || [], outgoingConnections, error: result.error, arguments: (node?.data as any)?.arguments || null, duration: result.completedAt && result.startedAt ? Math.round((new Date(result.completedAt).getTime() - new Date(result.startedAt).getTime()) / 1000) : null, }; }), connections: workflow?.edges.map(edge => ({ from: edge.source, to: edge.target, label: edge.label || null, fromNodeName: (workflow.nodes.find(n => n.id === edge.source)?.data as any)?.nodeName || edge.source, toNodeName: (workflow.nodes.find(n => n.id === edge.target)?.data as any)?.nodeName || edge.target, })) || [], }, totalDuration: execution?.completedAt && execution?.startedAt ? Math.round((new Date(execution.completedAt).getTime() - new Date(execution.startedAt).getTime()) / 1000) : null, }; navigator.clipboard.writeText(JSON.stringify(results, null, 2)); setCopiedAll(true); setTimeout(() => setCopiedAll(false), 2000); }; const handleRun = useCallback(() => { const input = inputVariables.length > 0 ? JSON.stringify(inputValues) : inputValues.input || ''; console.log('🚀 Running workflow with input:', input); setShowInput(false); onRun(input); }, [inputVariables, inputValues, onRun]); const hasMissingRequiredInputs = inputVariables.length > 0 ? inputVariables.some((variable: any) => variable.required && (inputValues[variable.name] === undefined || inputValues[variable.name] === '' || inputValues[variable.name] === null)) : false; const canTriggerShortcut = showInput && !isRunning && !hasMissingRequiredInputs; useEffect(() => { if (!canTriggerShortcut) return; const handleKeyDown = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); handleRun(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [canTriggerShortcut, handleRun]); const handleReset = () => { setShowInput(true); setInputValues( inputVariables.length > 0 ? inputVariables.reduce((acc: any, v: any) => { acc[v.name] = v.defaultValue || ''; return acc; }, {}) : { input: '' } ); }; const handleCopyError = (error: string, nodeId: string) => { navigator.clipboard.writeText(error); setCopiedError(nodeId); setTimeout(() => setCopiedError(null), 2000); }; return ( {/* Header */}

Preview Workflow

Environment: {environment === 'production' ? 'Production' : 'Draft'}
{/* Copy All Results Button - Show when results exist */} {!showInput && Object.keys(nodeResults).length > 0 && ( <> )}
{execution?.status === 'waiting-auth' && pendingAuth && pendingAuth.toolName === 'user-approval' && (

Workflow Paused

Approval Required

{pendingAuth.message || 'This workflow requires your approval to continue.'}

)} {execution?.status === 'waiting-auth' && pendingAuth && pendingAuth.toolName !== 'user-approval' && (

Authorization required for {pendingAuth.toolName}

{pendingAuth.message || 'Open the authorization link in a new tab, approve access, then resume the workflow.'}

{pendingAuth.authUrl ? ( Open authorization ) : ( Waiting for authorization link... )}
)} {/* Current Node Indicator - Only show when running */} {isRunning && currentNodeId && (() => { const node = workflow?.nodes.find(n => n.id === currentNodeId); const nodeData = node?.data as any; const nodeName = nodeData?.nodeName || nodeData?.label || 'Node'; const nodeType = nodeData?.nodeType || node?.type || 'agent'; const NodeIcon = getNodeIcon(nodeType); return (
{typeof nodeName === 'string' ? nodeName : 'Running...'}
); })()}
{/* Content */}
{showInput ? (
{/* Input Variables */} {inputVariables.length > 0 ? (

Workflow Inputs

{inputVariables.map((variable: any, index: number) => (
{variable.description && (

{variable.description}

)} {variable.type === 'boolean' ? ( ) : variable.type === 'number' ? ( setInputValues({ ...inputValues, [variable.name]: parseFloat(e.target.value) })} placeholder={variable.defaultValue || '0'} className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-accent-black transition-colors" /> ) : ( setInputValues({ ...inputValues, [variable.name]: e.target.value })} placeholder={variable.defaultValue || `Enter ${variable.name}...`} className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-accent-black transition-colors" /> )}
))}
) : (