import React, { useEffect, useState, useRef, useCallback } from 'react'; import { getDirective, DirectiveStep, DirectiveWithSteps } from '../../../services/directiveApi'; import { StepLogFeed } from './StepLogFeed'; import './StepsDiagram.css'; interface StepsDiagramComponentProps { directiveId: string; onExpandContract?: (step: DirectiveStep) => void; } type StepStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'skipped'; const STATUS_LABELS: Record = { pending: 'Queued', ready: 'Ready', running: 'Executing', completed: 'Fulfilled', failed: 'Failed', skipped: 'Skipped', }; function formatTime(dateStr: string): string { if (!dateStr) return ''; const d = new Date(dateStr); return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } interface StepCardProps { step: DirectiveStep; isExpanded: boolean; onToggleExpand: () => void; onCollapse: () => void; } function StepCard({ step, isExpanded, onToggleExpand, onCollapse }: StepCardProps) { const status = (step.status || 'pending').toLowerCase() as StepStatus; const hasTask = !!step.taskId || !!step.contractId; const canExpand = hasTask && ['running', 'completed', 'failed'].includes(status); return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggleExpand(); } } : undefined} > {step.name}
{STATUS_LABELS[status] || status} {canExpand && ( )}
{step.description && !isExpanded && (

{step.description}

)}
#{step.orderIndex} {status === 'running' && ( In progress... )} {status === 'completed' && step.completedAt && ( Completed {formatTime(step.completedAt)} )}
{/* Expandable log feed */} {isExpanded && hasTask && ( )}
); } export function StepsDiagramComponent({ directiveId, onExpandContract }: StepsDiagramComponentProps) { const [steps, setSteps] = useState([]); const [directiveStatus, setDirectiveStatus] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expandedStepId, setExpandedStepId] = useState(null); const intervalRef = useRef | null>(null); const prevStepCountRef = useRef(0); const fetchSteps = useCallback(async () => { try { const data: DirectiveWithSteps = await getDirective(directiveId); setSteps(data.steps || []); setDirectiveStatus(data.status || ''); setError(null); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load contracts'); } finally { setLoading(false); } }, [directiveId]); useEffect(() => { fetchSteps(); intervalRef.current = setInterval(fetchSteps, 5000); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [fetchSteps]); // Track when new steps appear for animation useEffect(() => { prevStepCountRef.current = steps.length; }, [steps.length]); // Keyboard shortcut: Escape to collapse expanded step useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === 'Escape' && expandedStepId) { setExpandedStepId(null); } }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, [expandedStepId]); const toggleExpand = useCallback((stepId: string) => { setExpandedStepId(prev => prev === stepId ? null : stepId); }, []); const collapseExpanded = useCallback(() => { setExpandedStepId(null); }, []); const completedCount = steps.filter(s => s.status?.toLowerCase() === 'completed').length; const totalCount = steps.length; const isActive = ['active', 'running', 'planning'].includes(directiveStatus.toLowerCase()); const isBuilding = isActive && steps.length === 0; // Group steps by orderIndex const groupedSteps: Map = new Map(); const sortedSteps = [...steps].sort((a, b) => a.orderIndex - b.orderIndex); for (const step of sortedSteps) { const idx = step.orderIndex; if (!groupedSteps.has(idx)) groupedSteps.set(idx, []); groupedSteps.get(idx)!.push(step); } const orderGroups = Array.from(groupedSteps.entries()).sort((a, b) => a[0] - b[0]); if (loading) { return (
Contract Steps Authored by Makima
Loading contracts...
); } if (error) { return (
Contract Steps Authored by Makima
Failed to load contracts: {error}
); } return (
Contract Steps {totalCount > 0 && ( {completedCount}/{totalCount} fulfilled )}
Authored by Makima
{isBuilding && (
Makima is drafting contracts...
)} {totalCount === 0 && !isBuilding && (
No contract steps defined yet.
)} {totalCount > 0 && (
{orderGroups.map(([orderIndex, groupSteps], groupIdx) => ( {groupIdx > 0 && (
)}
{groupSteps.map((step) => ( toggleExpand(step.id)} onCollapse={collapseExpanded} /> ))}
))}
)}
); }