summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/mesh/TaskTree.tsx
blob: 46ae78d6c697901e564245fdfdb05b61a15c6ad5 (plain) (tree)





































































































































































































































































































































































































                                                                                                                                                  
import { useState, useCallback } from "react";
import type { TaskSummary, TaskStatus } from "../../lib/api";

interface TaskTreeProps {
  tasks: TaskSummary[];
  supervisorTaskId: string | null;
  onSelect: (taskId: string) => void;
  onStartSupervisor?: () => void;
  loading?: boolean;
  fetchSubtasks?: (taskId: string) => Promise<TaskSummary[]>;
}

interface TreeNodeProps {
  task: TaskSummary;
  isSupervisorTask: boolean;
  onSelect: (taskId: string) => void;
  depth: number;
  fetchSubtasks?: (taskId: string) => Promise<TaskSummary[]>;
}

function getStatusColor(status: TaskStatus): string {
  switch (status) {
    case "pending":
      return "text-[#9bc3ff]";
    case "running":
      return "text-green-400";
    case "paused":
      return "text-yellow-400";
    case "blocked":
      return "text-orange-400";
    case "done":
      return "text-emerald-400";
    case "failed":
      return "text-red-400";
    case "merged":
      return "text-purple-400";
    default:
      return "text-[#9bc3ff]";
  }
}

function getStatusIcon(status: TaskStatus): string {
  switch (status) {
    case "pending":
      return "○";
    case "running":
      return "◉";
    case "paused":
      return "◎";
    case "blocked":
      return "◈";
    case "done":
      return "●";
    case "failed":
      return "✕";
    case "merged":
      return "◆";
    default:
      return "○";
  }
}

function TreeNode({ task, isSupervisorTask, onSelect, depth, fetchSubtasks }: TreeNodeProps) {
  const [expanded, setExpanded] = useState(isSupervisorTask); // Supervisor expanded by default
  const [children, setChildren] = useState<TaskSummary[] | null>(null);
  const [loadingChildren, setLoadingChildren] = useState(false);

  const hasSubtasks = task.subtaskCount > 0;

  const handleToggle = useCallback(async () => {
    if (!hasSubtasks) return;

    if (expanded) {
      setExpanded(false);
    } else {
      if (!children && fetchSubtasks) {
        setLoadingChildren(true);
        try {
          const subtasks = await fetchSubtasks(task.id);
          setChildren(subtasks);
        } catch (err) {
          console.error("Failed to fetch subtasks:", err);
        } finally {
          setLoadingChildren(false);
        }
      }
      setExpanded(true);
    }
  }, [expanded, children, hasSubtasks, task.id, fetchSubtasks]);

  const indent = depth * 16;

  return (
    <div className="select-none">
      <div
        className={`flex items-center gap-2 py-2 px-2 hover:bg-[rgba(117,170,252,0.05)] transition-colors cursor-pointer group ${
          isSupervisorTask ? "bg-[rgba(117,170,252,0.08)] border-l-2 border-[#75aafc]" : ""
        }`}
        style={{ paddingLeft: `${indent + 8}px` }}
      >
        {/* Expand/Collapse button */}
        <button
          onClick={handleToggle}
          className={`w-4 h-4 flex items-center justify-center font-mono text-[10px] ${
            hasSubtasks
              ? "text-[#75aafc] hover:text-[#9bc3ff]"
              : "text-transparent cursor-default"
          }`}
          disabled={!hasSubtasks}
        >
          {loadingChildren ? (
            <span className="animate-spin">...</span>
          ) : hasSubtasks ? (
            expanded ? "▼" : "▶"
          ) : (
            ""
          )}
        </button>

        {/* Supervisor badge or status icon */}
        {isSupervisorTask ? (
          <span className="px-1.5 py-0.5 font-mono text-[9px] bg-[#0f3c78] text-[#75aafc] border border-[#3f6fb3] uppercase">
            Supervisor
          </span>
        ) : (
          <span
            className={`font-mono text-xs ${getStatusColor(task.status)}`}
            title={task.status}
          >
            {getStatusIcon(task.status)}
          </span>
        )}

        {/* Task name - clickable */}
        <button
          onClick={() => onSelect(task.id)}
          className={`flex-1 text-left font-mono text-sm transition-colors truncate ${
            isSupervisorTask
              ? "text-[#9bc3ff] hover:text-white font-medium"
              : "text-[#dbe7ff] hover:text-white"
          }`}
        >
          {task.name}
        </button>

        {/* Status for supervisor */}
        {isSupervisorTask && (
          <span
            className={`font-mono text-[10px] ${getStatusColor(task.status)}`}
          >
            {task.status}
          </span>
        )}

        {/* Subtask count badge */}
        {hasSubtasks && !isSupervisorTask && (
          <span className="font-mono text-[9px] text-[#555] group-hover:text-[#75aafc]">
            {task.subtaskCount} sub
          </span>
        )}

        {/* Priority indicator */}
        {task.priority > 0 && (
          <span className="font-mono text-[9px] text-orange-400">
            P{task.priority}
          </span>
        )}
      </div>

      {/* Children */}
      {expanded && children && children.length > 0 && (
        <div className="border-l border-[rgba(117,170,252,0.15)]" style={{ marginLeft: `${indent + 16}px` }}>
          {children.map((child) => (
            <TreeNode
              key={child.id}
              task={child}
              isSupervisorTask={false}
              onSelect={onSelect}
              depth={depth + 1}
              fetchSubtasks={fetchSubtasks}
            />
          ))}
        </div>
      )}
    </div>
  );
}

// Stats summary component
export interface TaskTreeStats {
  total: number;
  pending: number;
  running: number;
  paused: number;
  blocked: number;
  done: number;
  failed: number;
  merged: number;
}

export function calculateTreeStats(tasks: TaskSummary[]): TaskTreeStats {
  const stats: TaskTreeStats = {
    total: tasks.length,
    pending: 0,
    running: 0,
    paused: 0,
    blocked: 0,
    done: 0,
    failed: 0,
    merged: 0,
  };

  for (const task of tasks) {
    // Skip supervisor task in stats
    if (task.isSupervisor) continue;

    switch (task.status) {
      case "pending":
        stats.pending++;
        break;
      case "running":
        stats.running++;
        break;
      case "paused":
        stats.paused++;
        break;
      case "blocked":
        stats.blocked++;
        break;
      case "done":
        stats.done++;
        break;
      case "failed":
        stats.failed++;
        break;
      case "merged":
        stats.merged++;
        break;
    }
  }

  // Adjust total to exclude supervisor
  stats.total = tasks.filter(t => !t.isSupervisor).length;

  return stats;
}

// Progress bar for task tree
export function TaskTreeProgressBar({ stats }: { stats: TaskTreeStats }) {
  if (stats.total === 0) return null;

  const completedPercent = ((stats.done + stats.merged) / stats.total) * 100;
  const runningPercent = (stats.running / stats.total) * 100;
  const failedPercent = (stats.failed / stats.total) * 100;

  return (
    <div className="space-y-2">
      {/* Progress bar */}
      <div className="h-2 bg-[rgba(117,170,252,0.1)] rounded overflow-hidden flex">
        <div
          className="bg-emerald-400 transition-all"
          style={{ width: `${completedPercent}%` }}
          title={`Completed: ${stats.done + stats.merged}`}
        />
        <div
          className="bg-green-400 transition-all"
          style={{ width: `${runningPercent}%` }}
          title={`Running: ${stats.running}`}
        />
        <div
          className="bg-red-400 transition-all"
          style={{ width: `${failedPercent}%` }}
          title={`Failed: ${stats.failed}`}
        />
      </div>

      {/* Summary */}
      <div className="flex items-center justify-between font-mono text-[10px]">
        <span className="text-[#555]">
          {stats.done + stats.merged} / {stats.total} completed
        </span>
        {stats.running > 0 && (
          <span className="text-green-400">{stats.running} running</span>
        )}
        {stats.failed > 0 && (
          <span className="text-red-400">{stats.failed} failed</span>
        )}
      </div>
    </div>
  );
}

export function TaskTree({
  tasks,
  supervisorTaskId,
  onSelect,
  onStartSupervisor,
  loading = false,
  fetchSubtasks,
}: TaskTreeProps) {
  if (loading) {
    return (
      <div className="p-4 text-center font-mono text-xs text-[#555]">
        Loading tasks...
      </div>
    );
  }

  // Separate supervisor from other tasks
  const supervisorTask = tasks.find(t => t.id === supervisorTaskId || t.isSupervisor);
  const workerTasks = tasks.filter(t => t.id !== supervisorTaskId && !t.isSupervisor && !t.parentTaskId);

  // Calculate stats for worker tasks
  const stats = calculateTreeStats(tasks);

  return (
    <div className="space-y-4">
      {/* Supervisor Section */}
      <div className="space-y-2">
        <div className="flex items-center justify-between">
          <h3 className="font-mono text-xs text-[#75aafc] uppercase">
            Contract Supervisor
          </h3>
          {supervisorTask && supervisorTask.status === "pending" && onStartSupervisor && (
            <button
              onClick={onStartSupervisor}
              className="px-2 py-1 font-mono text-[10px] text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors"
            >
              Start Supervisor
            </button>
          )}
        </div>

        {supervisorTask ? (
          <TreeNode
            task={supervisorTask}
            isSupervisorTask={true}
            onSelect={onSelect}
            depth={0}
            fetchSubtasks={fetchSubtasks}
          />
        ) : (
          <div className="p-3 border border-dashed border-[rgba(117,170,252,0.2)] text-center">
            <p className="font-mono text-xs text-[#555]">
              No supervisor task found
            </p>
          </div>
        )}
      </div>

      {/* Progress Section */}
      {stats.total > 0 && (
        <div className="space-y-2">
          <h3 className="font-mono text-xs text-[#75aafc] uppercase">
            Task Progress
          </h3>
          <TaskTreeProgressBar stats={stats} />
        </div>
      )}

      {/* Worker Tasks Section */}
      {workerTasks.length > 0 && (
        <div className="space-y-2">
          <h3 className="font-mono text-xs text-[#75aafc] uppercase">
            Worker Tasks ({workerTasks.length})
          </h3>
          <div className="divide-y divide-[rgba(117,170,252,0.1)]">
            {workerTasks.map((task) => (
              <TreeNode
                key={task.id}
                task={task}
                isSupervisorTask={false}
                onSelect={onSelect}
                depth={0}
                fetchSubtasks={fetchSubtasks}
              />
            ))}
          </div>
        </div>
      )}

      {/* Empty State */}
      {workerTasks.length === 0 && !supervisorTask && (
        <div className="p-4 text-center font-mono text-xs text-[#555]">
          No tasks in this contract
        </div>
      )}
    </div>
  );
}