summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/directives/DirectiveDetail.tsx
blob: 094cdf2740628630d54a62405562ef9d00e27e9c (plain) (tree)
1
2
3
4
5
6
7
8
9

                                           


                      

                 
                       
                                             




                                  
                                 
                                                     











                                                       


















                                                               
          















                                                                                        
            







                                                                                                     
        




















                                                                                                      
          










                                                                                  
            






                                                                                

































                                                                                                          
          
            
                          


































                                                                          

















                                                                                           




                                                                                    


                                                                 

















                                                                                                                 







                                                            





















                                                                                                                                       


































































                                                                                             


                                                                                          


                                       

                                                               







                  
import { useEffect, useRef } from "react";
import { useNavigate } from "react-router";
import type {
  DirectiveWithChains,
  DirectiveStatus,
  ChainWithSteps,
  ChainStep,
} from "../../lib/api";
import { getDirective } from "../../lib/api";

interface DirectiveDetailProps {
  directive: DirectiveWithChains;
  onBack: () => void;
  onDelete?: (id: string) => void;
  onStart?: (id: string) => void;
  onRefresh?: (updated: DirectiveWithChains) => void;
}

const statusColors: Record<DirectiveStatus, string> = {
  draft: "text-[#888]",
  planning: "text-yellow-400",
  active: "text-green-400",
  paused: "text-orange-400",
  completed: "text-blue-400",
  archived: "text-[#555]",
  failed: "text-red-400",
};

const stepStatusColors: Record<string, string> = {
  pending: "text-[#888]",
  running: "text-yellow-400",
  passed: "text-green-400",
  failed: "text-red-400",
};

const stepStatusIcons: Record<string, string> = {
  pending: "\u25CB", // ○
  running: "\u25D4", // ◔
  passed: "\u25CF",  // ●
  failed: "\u2715",  // ✕
};

function StepRow({ step }: { step: ChainStep }) {
  const navigate = useNavigate();
  const color = stepStatusColors[step.status] || "text-[#888]";
  const icon = stepStatusIcons[step.status] || "\u25CB";

  return (
    <div className="flex items-start gap-2 py-1 px-2 hover:bg-[rgba(117,170,252,0.05)]">
      <span className={`font-mono text-[11px] ${color} mt-px`}>{icon}</span>
      <div className="flex-1 min-w-0">
        <div className="flex items-center gap-2">
          <span className="font-mono text-[11px] text-[#dbe7ff] truncate">
            {step.name}
          </span>
          <span className={`font-mono text-[9px] uppercase ${color}`}>
            {step.status}
          </span>
        </div>
        {step.description && (
          <p className="font-mono text-[10px] text-[#7788aa] truncate">
            {step.description}
          </p>
        )}
      </div>
      {step.contractId && (
        <button
          onClick={() => navigate(`/contracts/${step.contractId}`)}
          className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors shrink-0"
          title="View contract"
        >
          contract &rarr;
        </button>
      )}
    </div>
  );
}

function ChainCard({ chainWithSteps }: { chainWithSteps: ChainWithSteps }) {
  const chain = chainWithSteps;
  const steps = chainWithSteps.steps || [];

  return (
    <div className="border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)]">
      <div className="p-3">
        <div className="flex items-center justify-between mb-1">
          <span className="font-mono text-xs text-[#dbe7ff]">{chain.name}</span>
          <span className="font-mono text-[10px] text-[#7788aa] uppercase">
            gen {chain.generation} &middot; {chain.status}
          </span>
        </div>
        {chain.description && (
          <p className="font-mono text-[11px] text-[#7788aa] mb-1">
            {chain.description}
          </p>
        )}
        <div className="flex gap-3 font-mono text-[10px] text-[#7788aa]">
          <span>
            {chain.completedSteps}/{chain.totalSteps} steps
          </span>
          {chain.failedSteps > 0 && (
            <span className="text-red-400">{chain.failedSteps} failed</span>
          )}
          {chain.currentConfidence != null && (
            <span>confidence: {(chain.currentConfidence * 100).toFixed(0)}%</span>
          )}
        </div>
      </div>
      {steps.length > 0 && (
        <div className="border-t border-dashed border-[rgba(117,170,252,0.15)]">
          {steps.map((step) => (
            <StepRow key={step.id} step={step} />
          ))}
        </div>
      )}
    </div>
  );
}

function JsonSection({
  label,
  data,
}: {
  label: string;
  data: unknown[] | unknown;
}) {
  const items = Array.isArray(data) ? data : [];
  if (items.length === 0) return null;

  return (
    <div>
      <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1">
        {label}
      </h4>
      <div className="font-mono text-xs text-[#9bb8d8] bg-[rgba(0,0,0,0.2)] p-2 max-h-32 overflow-y-auto">
        {items.map((item, i) => (
          <div key={i} className="mb-0.5">
            {typeof item === "string" ? item : JSON.stringify(item)}
          </div>
        ))}
      </div>
    </div>
  );
}

export function DirectiveDetail({
  directive,
  onBack,
  onDelete,
  onStart,
  onRefresh,
}: DirectiveDetailProps) {
  const navigate = useNavigate();

  // Auto-poll when directive is in an active state
  const isLive =
    directive.status === "planning" || directive.status === "active";
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  useEffect(() => {
    if (!isLive) {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
      return;
    }

    intervalRef.current = setInterval(async () => {
      try {
        const updated = await getDirective(directive.id);
        if (updated && onRefresh) {
          onRefresh(updated);
        }
      } catch {
        // Ignore poll errors
      }
    }, 5000);

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
    };
  }, [isLive, directive.id, onRefresh]);

  return (
    <div className="panel h-full flex flex-col">
      {/* Header */}
      <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
        <div className="flex items-center gap-2 mb-2">
          <button
            onClick={onBack}
            className="font-mono text-xs text-[#75aafc] hover:text-white transition-colors"
          >
            &larr; Back
          </button>
          <span
            className={`font-mono text-[10px] uppercase ${
              statusColors[directive.status as DirectiveStatus] || "text-[#888]"
            }`}
          >
            {directive.status}
          </span>
          {isLive && (
            <span className="font-mono text-[9px] text-yellow-400/60 animate-pulse">
              polling
            </span>
          )}
          <span className="font-mono text-[10px] text-[#7788aa]">
            v{directive.version}
          </span>
          <div className="ml-auto flex gap-2">
            {onStart && directive.status === "draft" && (
              <button
                onClick={() => onStart(directive.id)}
                className="font-mono text-[10px] text-green-400 hover:text-green-300 transition-colors uppercase"
              >
                Start
              </button>
            )}
            {onDelete && (
              <button
                onClick={() => onDelete(directive.id)}
                className="font-mono text-[10px] text-red-400 hover:text-red-300 transition-colors uppercase"
              >
                Delete
              </button>
            )}
          </div>
        </div>
        <h2 className="font-mono text-sm text-[#dbe7ff]">
          {directive.title}
        </h2>
      </div>

      {/* Content */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {/* Orchestrator contract link */}
        {directive.orchestratorContractId && (
          <div className="flex items-center gap-2 p-2 border border-dashed border-[rgba(117,170,252,0.2)] bg-[rgba(117,170,252,0.03)]">
            <span className="font-mono text-[10px] text-[#7788aa] uppercase">
              Planning Contract
            </span>
            <button
              onClick={() =>
                navigate(`/contracts/${directive.orchestratorContractId}`)
              }
              className="font-mono text-[11px] text-[#75aafc] hover:text-white transition-colors"
            >
              {directive.orchestratorContractId.slice(0, 8)}... &rarr;
            </button>
            {directive.status === "planning" && (
              <span className="font-mono text-[9px] text-yellow-400 animate-pulse">
                planning in progress
              </span>
            )}
          </div>
        )}

        {/* Goal */}
        <div>
          <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1">
            Goal
          </h4>
          <p className="font-mono text-xs text-[#9bb8d8] whitespace-pre-wrap">
            {directive.goal}
          </p>
        </div>

        {/* Config */}
        <div className="grid grid-cols-2 gap-2">
          <div>
            <span className="font-mono text-[10px] text-[#7788aa] uppercase">
              Autonomy
            </span>
            <div className="font-mono text-xs text-[#dbe7ff]">
              {directive.autonomyLevel}
            </div>
          </div>
          <div>
            <span className="font-mono text-[10px] text-[#7788aa] uppercase">
              Chains
            </span>
            <div className="font-mono text-xs text-[#dbe7ff]">
              {directive.chainGenerationCount} generated
            </div>
          </div>
          <div>
            <span className="font-mono text-[10px] text-[#7788aa] uppercase">
              Cost
            </span>
            <div className="font-mono text-xs text-[#dbe7ff]">
              ${directive.totalCostUsd.toFixed(2)}
            </div>
          </div>
          {directive.repositoryUrl && (
            <div>
              <span className="font-mono text-[10px] text-[#7788aa] uppercase">
                Repository
              </span>
              <div className="font-mono text-xs text-[#dbe7ff] truncate">
                {directive.repositoryUrl}
              </div>
            </div>
          )}
        </div>

        {/* Structured sections */}
        <JsonSection label="Requirements" data={directive.requirements} />
        <JsonSection
          label="Acceptance Criteria"
          data={directive.acceptanceCriteria}
        />
        <JsonSection label="Constraints" data={directive.constraints} />
        <JsonSection
          label="External Dependencies"
          data={directive.externalDependencies}
        />

        {/* Chains */}
        <div>
          <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-2">
            Chains ({directive.chains.length})
          </h4>
          {directive.chains.length === 0 ? (
            <p className="font-mono text-xs text-[#7788aa]">
              {directive.status === "planning"
                ? "Planning in progress... chains will appear when the planner completes."
                : "No chains yet. Chains are created during planning."}
            </p>
          ) : (
            <div className="space-y-2">
              {directive.chains.map((cws) => (
                <ChainCard key={cws.id} chainWithSteps={cws} />
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}