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


                      

                 
                
                       
                                             


                                                                        




                                  
                                 
                                                     

 

                                              









                                                       









                                                  

                          





                                                               
                                       
 
          
                                                                                          









                                                                            




























                                                                                                     
                              
                                                                              


                              
            











                                                                                                      


                                                             







                                                                           
          







                                                                            


                                                                       

              
            






                                                                                































                                                                                                          
         
           
          
            
                          
                                 
                                                              

































                                                                          
























                                                                      



                                                                                  



                                                                                               
           


                                                   


                                                         
                                                                                                                                                            






                                                      
                                                                                                                                                   




                       
              



















                                                                                    

            











                                                                                    
               





                                                                

                     


                   
 




































                                                                                                                                           
 







                                                                                                 
                  




































                                                                                   
                  


























                                                                                                   
                  













                                                                              
                 





























                                                                                                 

                    

                
 












                                                                                                   
 



















                                                                                                 
                  





                                                         



            
import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router";
import type {
  DirectiveWithChains,
  DirectiveStatus,
  ChainWithSteps,
  ChainStep,
  ContractPhase,
} from "../../lib/api";
import { getDirective } from "../../lib/api";
import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar";
import { StepDiagram } from "./StepDiagram";
import { DirectiveContractsTab } from "./DirectiveContractsTab";

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

type Tab = "overview" | "chain" | "contracts";

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";
  const summary = step.contractSummary;

  return (
    <div className="flex items-start gap-2 py-1.5 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>
        {summary && (
          <div className="flex items-center gap-2 mt-0.5">
            <PhaseProgressBarCompact
              currentPhase={summary.phase as ContractPhase}
            />
            <span className="font-mono text-[9px] text-[#7788aa]">
              {summary.tasksDone}/{summary.taskCount} tasks
            </span>
            {step.contractId && (
              <button
                onClick={(e) => {
                  e.stopPropagation();
                  navigate(`/contracts/${step.contractId}`);
                }}
                className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors"
              >
                contract &rarr;
              </button>
            )}
          </div>
        )}
        {!summary && step.contractId && (
          <button
            onClick={() => navigate(`/contracts/${step.contractId}`)}
            className="font-mono text-[9px] text-[#75aafc] hover:text-white transition-colors mt-0.5"
          >
            contract &rarr;
          </button>
        )}
        {step.description && (
          <p className="font-mono text-[10px] text-[#7788aa] truncate mt-0.5">
            {step.description}
          </p>
        )}
      </div>
    </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();
  const [activeTab, setActiveTab] = useState<Tab>("overview");

  // 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]);

  // Count total steps and completed steps across all chains
  const totalSteps = directive.chains.reduce(
    (sum, c) => sum + c.totalSteps,
    0
  );
  const completedSteps = directive.chains.reduce(
    (sum, c) => sum + c.completedSteps,
    0
  );

  // Count contracts
  const contractCount =
    (directive.orchestratorContractSummary ? 1 : 0) +
    directive.chains.reduce(
      (sum, c) =>
        sum + c.steps.filter((s) => s.contractSummary != null).length,
      0
    );

  const tabs: { key: Tab; label: string; count?: number }[] = [
    { key: "overview", label: "Overview" },
    { key: "chain", label: "Chain", count: totalSteps },
    { key: "contracts", label: "Contracts", count: contractCount },
  ];

  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 justify-between mb-3">
          <button
            onClick={onBack}
            className="font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
          >
            &larr; Back to list
          </button>
          <div className="flex items-center gap-2">
            {onStart && directive.status === "draft" && (
              <button
                onClick={() => onStart(directive.id)}
                className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
              >
                Start
              </button>
            )}
            {onDelete && (
              <button
                onClick={() => onDelete(directive.id)}
                className="px-3 py-1.5 font-mono text-xs text-red-400 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
              >
                Delete
              </button>
            )}
          </div>
        </div>
        <div className="flex items-center gap-3 mb-2">
          <h2 className="font-mono text-lg text-[#dbe7ff]">
            {directive.title}
          </h2>
          <span
            className={`font-mono text-xs 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>
      </div>

      {/* Tabs */}
      <div className="flex border-b border-[rgba(117,170,252,0.2)]">
        {tabs.map((tab) => (
          <button
            key={tab.key}
            onClick={() => setActiveTab(tab.key)}
            className={`
              px-4 py-2 font-mono text-xs uppercase tracking-wider transition-colors
              ${
                activeTab === tab.key
                  ? "text-[#dbe7ff] border-b-2 border-[#75aafc]"
                  : "text-[#555] hover:text-[#9bc3ff]"
              }
            `}
          >
            {tab.label}
            {tab.count != null && tab.count > 0 && (
              <span className="ml-1 text-[10px] text-[#7788aa]">
                ({tab.count})
              </span>
            )}
          </button>
        ))}
      </div>

      {/* Tab content */}
      <div className="flex-1 overflow-y-auto p-4">
        {activeTab === "overview" && (
          <div className="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>
                {directive.orchestratorContractSummary && (
                  <PhaseProgressBarCompact
                    currentPhase={
                      directive.orchestratorContractSummary
                        .phase as ContractPhase
                    }
                  />
                )}
                <button
                  onClick={() =>
                    navigate(
                      `/contracts/${directive.orchestratorContractId}`
                    )
                  }
                  className="font-mono text-[11px] text-[#75aafc] hover:text-white transition-colors"
                >
                  {directive.orchestratorContractSummary?.name ||
                    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 grid */}
            <div className="grid grid-cols-3 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 className="col-span-3">
                  <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>

            {/* Stat cards */}
            <div className="grid grid-cols-3 gap-2">
              <div className="border border-dashed border-[rgba(117,170,252,0.2)] p-2 text-center">
                <div className="font-mono text-lg text-[#dbe7ff]">
                  {totalSteps}
                </div>
                <div className="font-mono text-[9px] text-[#7788aa] uppercase">
                  Total Steps
                </div>
              </div>
              <div className="border border-dashed border-[rgba(117,170,252,0.2)] p-2 text-center">
                <div className="font-mono text-lg text-green-400">
                  {completedSteps}
                </div>
                <div className="font-mono text-[9px] text-[#7788aa] uppercase">
                  Completed
                </div>
              </div>
              <div className="border border-dashed border-[rgba(117,170,252,0.2)] p-2 text-center">
                <div className="font-mono text-lg text-[#dbe7ff]">
                  ${directive.totalCostUsd.toFixed(2)}
                </div>
                <div className="font-mono text-[9px] text-[#7788aa] uppercase">
                  Cost
                </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}
            />

            {/* Metadata */}
            <div>
              <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1">
                Metadata
              </h4>
              <div className="grid grid-cols-2 gap-1 font-mono text-[10px]">
                <span className="text-[#7788aa]">Created</span>
                <span className="text-[#9bb8d8]">
                  {new Date(directive.createdAt).toLocaleString()}
                </span>
                <span className="text-[#7788aa]">Updated</span>
                <span className="text-[#9bb8d8]">
                  {new Date(directive.updatedAt).toLocaleString()}
                </span>
                {directive.startedAt && (
                  <>
                    <span className="text-[#7788aa]">Started</span>
                    <span className="text-[#9bb8d8]">
                      {new Date(directive.startedAt).toLocaleString()}
                    </span>
                  </>
                )}
                {directive.completedAt && (
                  <>
                    <span className="text-[#7788aa]">Completed</span>
                    <span className="text-[#9bb8d8]">
                      {new Date(directive.completedAt).toLocaleString()}
                    </span>
                  </>
                )}
                <span className="text-[#7788aa]">Version</span>
                <span className="text-[#9bb8d8]">{directive.version}</span>
              </div>
            </div>
          </div>
        )}

        {activeTab === "chain" && (
          <div className="space-y-4">
            {/* Step diagram */}
            {directive.chains.length > 0 && (
              <div>
                <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-2">
                  Step Dependencies
                </h4>
                <StepDiagram
                  steps={directive.chains.flatMap((c) => c.steps)}
                />
              </div>
            )}

            {/* Chain cards */}
            <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."
                    : directive.status === "draft"
                      ? "No chains yet. Start the directive to begin planning."
                      : "No chains created for this directive."}
                </p>
              ) : (
                <div className="space-y-2">
                  {directive.chains.map((cws) => (
                    <ChainCard key={cws.id} chainWithSteps={cws} />
                  ))}
                </div>
              )}
            </div>
          </div>
        )}

        {activeTab === "contracts" && (
          <DirectiveContractsTab directive={directive} />
        )}
      </div>
    </div>
  );
}