summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/contracts/AutopilotPanel.tsx
blob: a8a8e2ee1d87449772cbf9cc6545e473fe3d7d5f (plain) (tree)















































































































































































































                                                                                                                                                                                                        
import { useState, useCallback } from "react";
import type { ContractWithRelations } from "../../lib/api";
import {
  getSupervisorStatus,
  startSupervisor,
  stopSupervisor,
  resumeSupervisor,
  type SupervisorStatus,
} from "../../lib/api";

interface AutopilotPanelProps {
  contract: ContractWithRelations;
  onUpdate: () => void;
}

const statusConfig: Record<
  SupervisorStatus["status"],
  { label: string; color: string; bgColor: string }
> = {
  not_configured: {
    label: "Not Configured",
    color: "text-[#555]",
    bgColor: "bg-[#555]/10",
  },
  pending: {
    label: "Ready",
    color: "text-yellow-400",
    bgColor: "bg-yellow-400/10",
  },
  starting: {
    label: "Starting...",
    color: "text-blue-400",
    bgColor: "bg-blue-400/10",
  },
  running: {
    label: "Running",
    color: "text-green-400",
    bgColor: "bg-green-400/10",
  },
  paused: {
    label: "Paused",
    color: "text-orange-400",
    bgColor: "bg-orange-400/10",
  },
  done: {
    label: "Completed",
    color: "text-blue-400",
    bgColor: "bg-blue-400/10",
  },
  failed: {
    label: "Failed",
    color: "text-red-400",
    bgColor: "bg-red-400/10",
  },
};

export function AutopilotPanel({ contract, onUpdate }: AutopilotPanelProps) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const supervisorStatus = getSupervisorStatus(contract);
  const config = statusConfig[supervisorStatus.status];

  const handleStart = useCallback(async () => {
    if (!supervisorStatus.supervisorTaskId) return;

    setLoading(true);
    setError(null);

    try {
      await startSupervisor(supervisorStatus.supervisorTaskId);
      onUpdate();
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to start autopilot");
    } finally {
      setLoading(false);
    }
  }, [supervisorStatus.supervisorTaskId, onUpdate]);

  const handleStop = useCallback(async () => {
    if (!supervisorStatus.supervisorTaskId) return;

    setLoading(true);
    setError(null);

    try {
      await stopSupervisor(supervisorStatus.supervisorTaskId);
      onUpdate();
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to stop autopilot");
    } finally {
      setLoading(false);
    }
  }, [supervisorStatus.supervisorTaskId, onUpdate]);

  const handleResume = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      await resumeSupervisor(contract.id, { resumeMode: "continue" });
      // After resuming, we need to start the task
      if (supervisorStatus.supervisorTaskId) {
        await startSupervisor(supervisorStatus.supervisorTaskId);
      }
      onUpdate();
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to resume autopilot");
    } finally {
      setLoading(false);
    }
  }, [contract.id, supervisorStatus.supervisorTaskId, onUpdate]);

  // Don't show panel for task-type contracts (they don't have supervisors)
  if (contract.contractType === "task") {
    return null;
  }

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between">
        <h3 className="font-mono text-xs text-[#75aafc] uppercase">
          Autopilot Mode
        </h3>
        <div
          className={`px-2 py-1 rounded font-mono text-xs ${config.color} ${config.bgColor}`}
        >
          {config.label}
        </div>
      </div>

      <p className="font-mono text-xs text-[#555]">
        {supervisorStatus.status === "not_configured" ? (
          "This contract does not have an autopilot supervisor configured."
        ) : supervisorStatus.status === "running" ? (
          "Autopilot is actively working on this contract, spawning tasks and managing progress."
        ) : supervisorStatus.status === "pending" ? (
          "Autopilot is ready to start. Click 'Enable' to begin autonomous work."
        ) : supervisorStatus.status === "paused" ? (
          "Autopilot is paused. Click 'Resume' to continue work."
        ) : supervisorStatus.status === "failed" ? (
          "Autopilot encountered an error. You can resume to retry."
        ) : supervisorStatus.status === "done" ? (
          "Autopilot has completed its work on this contract."
        ) : (
          "Autopilot is initializing..."
        )}
      </p>

      {error && (
        <div className="px-3 py-2 bg-red-500/10 border border-red-400/30 font-mono text-xs text-red-400">
          {error}
        </div>
      )}

      <div className="flex gap-2">
        {supervisorStatus.canStart && (
          <button
            onClick={handleStart}
            disabled={loading}
            className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-green-600/20 border border-green-400/50 hover:bg-green-600/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {loading ? "Starting..." : "Enable Autopilot"}
          </button>
        )}

        {supervisorStatus.canResume && (
          <button
            onClick={handleResume}
            disabled={loading}
            className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-blue-600/20 border border-blue-400/50 hover:bg-blue-600/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {loading ? "Resuming..." : "Resume Autopilot"}
          </button>
        )}

        {supervisorStatus.canStop && (
          <button
            onClick={handleStop}
            disabled={loading}
            className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-orange-600/20 border border-orange-400/50 hover:bg-orange-600/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {loading ? "Stopping..." : "Pause Autopilot"}
          </button>
        )}
      </div>

      {/* Show running indicator when active */}
      {supervisorStatus.status === "running" && (
        <div className="flex items-center gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.2)]">
          <div className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
          <span className="font-mono text-xs text-green-400">
            Autopilot is actively working
          </span>
        </div>
      )}

      {supervisorStatus.status === "starting" && (
        <div className="flex items-center gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.2)]">
          <div className="w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
          <span className="font-mono text-xs text-blue-400">
            Initializing autopilot...
          </span>
        </div>
      )}
    </div>
  );
}