summaryrefslogblamecommitdiff
path: root/frontend/src/components/document/nodes/StepsDiagramComponent.tsx
blob: 606c0ab9e0954c6325fec4243b06d533ce74a36a (plain) (tree)



















































































































































































                                                                                                 
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>
  );
}