import React, { useEffect, useState, useRef, useCallback } from 'react';
import { getDirective, DirectiveStep, DirectiveWithSteps } from '../../../services/directiveApi';
import './StepsDiagram.css';
interface StepsDiagramComponentProps {
directiveId: string;
}
type StepStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'skipped';
const STATUS_LABELS: Record<string, string> = {
pending: 'Pending',
ready: 'Ready',
running: 'Running',
completed: 'Done',
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' });
}
function StepCard({ step }: { step: DirectiveStep }) {
const status = (step.status || 'pending').toLowerCase() as StepStatus;
return (
<div className={`steps-diagram-card steps-diagram-card--${status}`}>
<div className="steps-diagram-card-header">
<span className="steps-diagram-card-name">{step.name}</span>
<span className={`steps-diagram-status-badge steps-diagram-status-badge--${status}`}>
{STATUS_LABELS[status] || status}
</span>
</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>
);
}
export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProps) {
const [steps, setSteps] = useState<DirectiveStep[]>([]);
const [directiveStatus, setDirectiveStatus] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | 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 steps');
} 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]);
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;
const isAddingSteps = isActive && steps.length > 0 && steps.length > prevStepCountRef.current;
// 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">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 steps...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="steps-diagram" contentEditable={false}>
<div className="steps-diagram-header">
<span className="steps-diagram-header-title">Steps</span>
<span className="steps-diagram-header-author">Authored by Makima</span>
</div>
<div className="steps-diagram-error">Failed to load steps: {error}</div>
</div>
);
}
return (
<div className="steps-diagram" contentEditable={false}>
<div className="steps-diagram-header">
<div className="steps-diagram-header-left">
<span className="steps-diagram-header-title">Steps</span>
{totalCount > 0 && (
<span className="steps-diagram-header-count">
{completedCount}/{totalCount} completed
</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 building the plan...</span>
</div>
)}
{totalCount === 0 && !isBuilding && (
<div className="steps-diagram-empty">No 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} />
))}
</div>
</React.Fragment>
))}
</div>
)}
</div>
);
}