summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/directives/DirectiveList.tsx
blob: 38a7caa4454507ed2a1a0bf014eb2d32909db11d (plain) (tree)
1
2
3
4
                                          
                                                                       
                                                                                   
                                                              













                                                                                 




                                                    

 
                                                                                                                                                    
                                                        












                                                                                                        










                                                                        






























                                                                                                                                                                                                           
                                                              







                                                                                                                                                       


                                                                                                                                                                                   


























                                                                                                           














                                                             


          
import { useState, useMemo } from "react";
import type { DirectiveSummary, DirectiveStatus } from "../../lib/api";
import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext";
import { DirectiveContextMenu } from "./DirectiveContextMenu";

const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = {
  draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" },
  active: { color: "text-green-400 border-green-800", label: "ACTIVE" },
  idle: { color: "text-yellow-400 border-yellow-800", label: "IDLE" },
  paused: { color: "text-orange-400 border-orange-800", label: "PAUSED" },
  archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
};

interface DirectiveListProps {
  directives: DirectiveSummary[];
  selectedId: string | null;
  onSelect: (id: string) => void;
  onCreate: () => void;
  onStart?: (directive: DirectiveSummary) => void;
  onPause?: (directive: DirectiveSummary) => void;
  onArchive?: (directive: DirectiveSummary) => void;
  onDelete?: (directive: DirectiveSummary) => void;
  onGoToPR?: (directive: DirectiveSummary) => void;
}

export function DirectiveList({ directives, selectedId, onSelect, onCreate, onStart, onPause, onArchive, onDelete, onGoToPR }: DirectiveListProps) {
  const { pendingQuestions } = useSupervisorQuestions();
  const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
  const [contextMenuDirective, setContextMenuDirective] = useState<DirectiveSummary | null>(null);

  const handleContextMenu = (e: React.MouseEvent, directive: DirectiveSummary) => {
    e.preventDefault();
    setContextMenuPosition({ x: e.clientX, y: e.clientY });
    setContextMenuDirective(directive);
  };

  const closeContextMenu = () => {
    setContextMenuPosition(null);
    setContextMenuDirective(null);
  };

  const questionsPerDirective = useMemo(() => {
    const counts = new Map<string, number>();
    for (const q of pendingQuestions) {
      if (q.directiveId) {
        counts.set(q.directiveId, (counts.get(q.directiveId) || 0) + 1);
      }
    }
    return counts;
  }, [pendingQuestions]);

  return (
    <div className="flex flex-col h-full">
      <div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]">
        <span className="text-[11px] font-mono text-[#9bc3ff] uppercase tracking-wide">
          Directives
        </span>
        <button
          type="button"
          onClick={onCreate}
          className="text-[11px] font-mono text-[#75aafc] hover:text-white bg-transparent border border-[rgba(117,170,252,0.3)] rounded px-2 py-0.5 hover:border-[rgba(117,170,252,0.6)] transition-colors"
        >
          + New
        </button>
      </div>
      <div className="flex-1 overflow-y-auto">
        {directives.length === 0 ? (
          <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
            No directives yet
          </div>
        ) : (
          directives.map((d) => {
            const badge = STATUS_BADGE[d.status] || STATUS_BADGE.draft;
            const progress = d.totalSteps > 0
              ? Math.round((d.completedSteps / d.totalSteps) * 100)
              : 0;

            return (
              <button
                key={d.id}
                type="button"
                onClick={() => onSelect(d.id)}
                onContextMenu={(e) => handleContextMenu(e, d)}
                className={`w-full text-left px-3 py-2.5 border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] transition-colors ${
                  selectedId === d.id ? "bg-[rgba(117,170,252,0.1)]" : ""
                }`}
              >
                <div className="flex items-center justify-between mb-1">
                  <span className="text-[12px] font-mono text-white truncate pr-2">
                    {d.title}
                  </span>
                  {questionsPerDirective.has(d.id) && (
                    <span className="inline-block w-2.5 h-2.5 rounded-full bg-amber-400 animate-pulse shrink-0" title={`${questionsPerDirective.get(d.id)} pending question(s)`} />
                  )}
                  <span
                    className={`text-[9px] font-mono ${badge.color} border rounded px-1.5 py-0.5 shrink-0`}
                  >
                    {badge.label}
                  </span>
                </div>
                <p className="text-[10px] text-[#7788aa] font-mono truncate mb-1.5">
                  {d.goal}
                </p>
                {d.totalSteps > 0 && (
                  <div className="flex items-center gap-2">
                    <div className="flex-1 h-1 bg-[#1a2540] rounded overflow-hidden">
                      <div
                        className="h-full bg-emerald-600 rounded transition-all"
                        style={{ width: `${progress}%` }}
                      />
                    </div>
                    <span className="text-[9px] font-mono text-[#556677] shrink-0">
                      {d.completedSteps}/{d.totalSteps}
                    </span>
                  </div>
                )}
              </button>
            );
          })
        )}
      </div>

      {/* Context Menu */}
      {contextMenuPosition && contextMenuDirective && (
        <DirectiveContextMenu
          x={contextMenuPosition.x}
          y={contextMenuPosition.y}
          directive={contextMenuDirective}
          onClose={closeContextMenu}
          onStart={() => onStart?.(contextMenuDirective)}
          onPause={() => onPause?.(contextMenuDirective)}
          onArchive={() => onArchive?.(contextMenuDirective)}
          onDelete={() => onDelete?.(contextMenuDirective)}
          onGoToPR={() => onGoToPR?.(contextMenuDirective)}
        />
      )}
    </div>
  );
}