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<string, string> = {
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 (
<div className={`steps-diagram-card steps-diagram-card--${status} ${isExpanded ? 'steps-diagram-card--expanded' : ''}`}>
<div
className={`steps-diagram-card-header ${canExpand ? 'steps-diagram-card-header--clickable' : ''}`}
onClick={canExpand ? onToggleExpand : undefined}
role={canExpand ? 'button' : undefined}
tabIndex={canExpand ? 0 : undefined}
onKeyDown={canExpand ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggleExpand(); } } : undefined}
>
<span className="steps-diagram-card-name">{step.name}</span>
<div className="steps-diagram-card-header-right">
<span className={`steps-diagram-status-badge steps-diagram-status-badge--${status}`}>
{STATUS_LABELS[status] || status}
</span>
{canExpand && (
<span className={`steps-diagram-expand-icon ${isExpanded ? 'expanded' : ''}`}>
▶
</span>
)}
</div>
</div>
{step.description && !isExpanded && (
<p className="steps-diagram-card-desc">{step.description}</p>
)}
<div className="steps-diagram-card-footer">
<span className="steps-diagram-card-index">#{step.orderIndex}</span>
{status === 'running' && (
<span className="steps-diagram-card-progress">In progress...</span>
)}
{status === 'completed' && step.completedAt && (
<span className="steps-diagram-card-time">
Completed {formatTime(step.completedAt)}
</span>
{hasTask && (
<button
className={`steps-diagram-card-expand-btn ${expanded ? 'steps-diagram-card-expand-btn--open' : ''}`}
onClick={() => setExpanded((v) => !v)}
title={expanded ? 'Collapse log feed' : 'Expand log feed'}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
)}
</div>
{step.description && (
<p className="steps-diagram-card-desc">{step.description}</p>
)}
<div className="steps-diagram-card-footer">
<span className="steps-diagram-card-index">#{step.orderIndex}</span>
{status === 'running' && (
<span className="steps-diagram-card-progress">In progress...</span>
)}
{status === 'completed' && step.completedAt && (
<span className="steps-diagram-card-time">
Completed {formatTime(step.completedAt)}
</span>
)}
</div>
</div>
{/* Expandable log feed */}
{isExpanded && hasTask && (
<StepLogFeed
taskId={step.taskId || step.contractId}
stepName={step.name}
stepStatus={status}
onCollapse={onCollapse}
/>
)}
</div>
);
}
export function StepsDiagramComponent({ directiveId, onExpandContract }: StepsDiagramComponentProps) {
const [steps, setSteps] = useState<DirectiveStep[]>([]);
const [directiveStatus, setDirectiveStatus] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [expandedStepId, setExpandedStepId] = useState<string | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const prevStepCountRef = useRef(0);
const toggleStep = useCallback((stepId: string) => {
setExpandedSteps((prev) => {
const next = new Set(prev);
if (next.has(stepId)) {
next.delete(stepId);
} else {
next.add(stepId);
}
return next;
});
}, []);
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<number, DirectiveStep[]> = 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 (
<div className="steps-diagram" contentEditable={false}>
<div className="steps-diagram-header">
<span className="steps-diagram-header-title">Contract Steps</span>
<span className="steps-diagram-header-author">Authored by Makima</span>
</div>
<div className="steps-diagram-loading">
<div className="steps-diagram-spinner" />
<span>Loading contracts...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="steps-diagram" contentEditable={false}>
<div className="steps-diagram-header">
<span className="steps-diagram-header-title">Contract Steps</span>
<span className="steps-diagram-header-author">Authored by Makima</span>
</div>
<div className="steps-diagram-error">Failed to load contracts: {error}</div>
</div>
);
}
return (
<div className={`steps-diagram ${expandedStepId ? 'steps-diagram--has-expanded' : ''}`} contentEditable={false}>
<div className="steps-diagram-header">
<div className="steps-diagram-header-left">
<span className="steps-diagram-header-title">Contract Steps</span>
{totalCount > 0 && (
<span className="steps-diagram-header-count">
{completedCount}/{totalCount} fulfilled
</span>
)}
</div>
<span className="steps-diagram-header-author">Authored by Makima</span>
</div>
{isBuilding && (
<div className="steps-diagram-planning">
<div className="steps-diagram-planning-dots">
<span /><span /><span />
</div>
<span>Makima is drafting contracts...</span>
</div>
)}
{totalCount === 0 && !isBuilding && (
<div className="steps-diagram-empty">No contract steps defined yet.</div>
)}
{totalCount > 0 && (
<div className="steps-diagram-dag">
{orderGroups.map(([orderIndex, groupSteps], groupIdx) => (
<React.Fragment key={orderIndex}>
{groupIdx > 0 && (
<div className="steps-diagram-arrow">
<div className="steps-diagram-arrow-line" />
<div className="steps-diagram-arrow-head" />
</div>
)}
<div className="steps-diagram-group">
{groupSteps.map((step) => (
<StepCard
key={step.id}
step={step}
isExpanded={expandedStepId === step.id}
onToggleExpand={() => toggleExpand(step.id)}
onCollapse={collapseExpanded}
/>
))}
</div>
</React.Fragment>
))}
</div>
)}
</div>
);
}