summaryrefslogblamecommitdiff
path: root/makima/frontend/src/routes/directives.tsx
blob: 895c86a418014c2d0111cd18eb7e6868be9646f9 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                                         




                                                                           
                                           
                                                           
                                                  
                                                           
                                                                                                                                                           
 






                                                                              
                                          























                                                                              


                                                                                  
                                                                                                     
                                                                                                                                                                                       
                                                                                                                                                     




                                                      





























                                                                                       

















                                                                             








































                                                                       







































                                                                                                                                




                                            

































                                                                                                                                                       


























                                                                                                                                                                           







































                                                                                                                                                             
                               

                                       
                                 
                                           
                                   





                                                 














                                                                            
import { useState, useEffect, useCallback } from "react";
import { useParams, useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import { DirectiveList } from "../components/directives/DirectiveList";
import { DirectiveDetail } from "../components/directives/DirectiveDetail";
import { useDirectives, useDirective } from "../hooks/useDirectives";
import { useDogs } from "../hooks/useDogs";
import { useUserSettings } from "../hooks/useUserSettings";
import { useAuth } from "../contexts/AuthContext";
import DocumentDirectivesPage from "./document-directives";
import { getRepositorySuggestions, startDirective, pauseDirective, updateDirective, type RepositoryHistoryEntry, type DirectiveSummary } from "../lib/api";

/**
 * Top-level /directives route. Gates between the legacy tabular UI and the
 * Document Mode (POC) UI based on the user's settings flag.
 *
 * Both code paths support /directives/:id deep links — the param is read by
 * each branch independently via useParams.
 */
export default function DirectivesPage() {
  const { settings, loading: settingsLoading } = useUserSettings();

  // While settings are loading for the very first time, render nothing inside
  // a Masthead-wrapped shell so we don't briefly flash the legacy UI just to
  // swap to document mode a moment later.
  if (settingsLoading && !settings) {
    return (
      <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
        <Masthead showNav />
        <main className="flex-1 flex items-center justify-center">
          <p className="text-[#7788aa] font-mono text-sm">Loading...</p>
        </main>
      </div>
    );
  }

  if (settings?.documentModeEnabled) {
    return <DocumentDirectivesPage />;
  }

  return <LegacyDirectivesPage />;
}

function LegacyDirectivesPage() {
  const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
  const navigate = useNavigate();
  const { id: selectedId } = useParams<{ id: string }>();
  const { directives, loading: listLoading, create, remove, refresh: refreshList } = useDirectives();
  const { directive, refresh: refreshDetail, update, start, pause, advance, completeStep, failStep, skipStep, updateGoal, cleanup, pickUpOrders, createPR } = useDirective(selectedId);
  const { dogs, loading: dogsLoading, create: createDog, update: updateDog, remove: removeDog, pickUpOrders: pickUpDogOrders } = useDogs(selectedId);

  const [showCreate, setShowCreate] = useState(false);
  const [newTitle, setNewTitle] = useState("");
  const [newGoal, setNewGoal] = useState("");
  const [newRepoUrl, setNewRepoUrl] = useState("");
  const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]);
  const [showRepoSuggestions, setShowRepoSuggestions] = useState(false);

  // Fetch repository suggestions when create form opens
  useEffect(() => {
    if (showCreate) {
      getRepositorySuggestions("remote", undefined, 10)
        .then((res) => {
          setRepoSuggestions(res.entries);
          setShowRepoSuggestions(res.entries.length > 0);
        })
        .catch(() => {
          setRepoSuggestions([]);
          setShowRepoSuggestions(false);
        });
    } else {
      setRepoSuggestions([]);
      setShowRepoSuggestions(false);
    }
  }, [showCreate]);

  const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => {
    if (suggestion.repositoryUrl) {
      setNewRepoUrl(suggestion.repositoryUrl);
    }
    if (!newTitle.trim() && suggestion.name) {
      setNewTitle(suggestion.name);
    }
    setShowRepoSuggestions(false);
  }, [newTitle]);

  useEffect(() => {
    if (!authLoading && isAuthConfigured && !isAuthenticated) {
      navigate("/login");
    }
  }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);

  if (authLoading) {
    return (
      <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
        <Masthead showNav />
        <main className="flex-1 flex items-center justify-center">
          <p className="text-[#7788aa] font-mono text-sm">Loading...</p>
        </main>
      </div>
    );
  }

  const handleContextStart = async (directive: DirectiveSummary) => {
    try {
      await startDirective(directive.id);
      await refreshList();
    } catch (e) {
      console.error("Failed to start directive:", e);
    }
  };

  const handleContextPause = async (directive: DirectiveSummary) => {
    try {
      await pauseDirective(directive.id);
      await refreshList();
    } catch (e) {
      console.error("Failed to pause directive:", e);
    }
  };

  const handleContextArchive = async (directive: DirectiveSummary) => {
    try {
      await updateDirective(directive.id, { status: "archived" });
      await refreshList();
    } catch (e) {
      console.error("Failed to archive directive:", e);
    }
  };

  const handleContextDelete = async (directive: DirectiveSummary) => {
    if (!window.confirm("Delete this directive?")) return;
    try {
      await remove(directive.id);
      if (directive.id === selectedId) navigate("/directives");
    } catch (e) {
      console.error("Failed to delete:", e);
    }
  };

  const handleContextGoToPR = (directive: DirectiveSummary) => {
    if (directive.prUrl) window.open(directive.prUrl, "_blank");
  };

  const handleCreate = async () => {
    if (!newTitle.trim() || !newGoal.trim()) return;
    try {
      const d = await create({
        title: newTitle.trim(),
        goal: newGoal.trim(),
        repositoryUrl: newRepoUrl.trim() || undefined,
      });
      setShowCreate(false);
      setNewTitle("");
      setNewGoal("");
      setNewRepoUrl("");
      navigate(`/directives/${d.id}`);
    } catch (e) {
      console.error("Failed to create directive:", e);
    }
  };

  const handleDelete = async () => {
    if (!selectedId) return;
    if (!window.confirm("Delete this directive?")) return;
    try {
      await remove(selectedId);
      navigate("/directives");
    } catch (e) {
      console.error("Failed to delete:", e);
    }
  };

  return (
    <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
      <Masthead showNav />
      <main className="flex-1 flex overflow-hidden" style={{ height: "calc(100vh - 80px)" }}>
        {/* Left: List */}
        <div className="w-[280px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col">
          <DirectiveList
            directives={directives}
            selectedId={selectedId ?? null}
            onSelect={(id) => navigate(`/directives/${id}`)}
            onCreate={() => setShowCreate(true)}
            onStart={handleContextStart}
            onPause={handleContextPause}
            onArchive={handleContextArchive}
            onDelete={handleContextDelete}
            onGoToPR={handleContextGoToPR}
          />
        </div>

        {/* Right: Detail or Create */}
        <div className="flex-1 overflow-hidden">
          {showCreate ? (
            <div className="p-4 max-w-lg">
              <h2 className="text-[14px] font-mono text-white font-medium mb-4">
                New Directive
              </h2>
              <div className="flex flex-col gap-3">
                <div>
                  <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1">
                    Title
                  </label>
                  <input
                    value={newTitle}
                    onChange={(e) => setNewTitle(e.target.value)}
                    placeholder="Project title..."
                    className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white"
                  />
                </div>
                <div>
                  <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1">
                    Goal
                  </label>
                  <textarea
                    value={newGoal}
                    onChange={(e) => setNewGoal(e.target.value)}
                    placeholder="What should this directive accomplish?"
                    rows={4}
                    className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white resize-y"
                  />
                </div>
                {showRepoSuggestions && repoSuggestions.length > 0 && (
                  <div>
                    <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
                      Recent Repositories
                    </label>
                    <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto">
                      {repoSuggestions.map((suggestion) => (
                        <button
                          key={suggestion.id}
                          type="button"
                          onClick={() => applyRepoSuggestion(suggestion)}
                          className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
                        >
                          <div className="flex items-center justify-between">
                            <span className="text-[#9bc3ff] truncate">{suggestion.name}</span>
                            <span className="text-[10px] text-[#556677] ml-2">
                              {suggestion.useCount}×
                            </span>
                          </div>
                          <div className="text-[10px] text-[#556677] truncate">
                            {suggestion.repositoryUrl}
                          </div>
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                <div>
                  <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1">
                    Repository URL (optional)
                  </label>
                  <input
                    value={newRepoUrl}
                    onChange={(e) => setNewRepoUrl(e.target.value)}
                    placeholder="https://github.com/..."
                    className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white"
                  />
                </div>
                <div className="flex gap-2">
                  <button
                    type="button"
                    onClick={handleCreate}
                    disabled={!newTitle.trim() || !newGoal.trim()}
                    className="text-[11px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-3 py-1 disabled:opacity-50"
                  >
                    Create
                  </button>
                  <button
                    type="button"
                    onClick={() => setShowCreate(false)}
                    className="text-[11px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-3 py-1"
                  >
                    Cancel
                  </button>
                </div>
              </div>
            </div>
          ) : selectedId && directive ? (
            <DirectiveDetail
              directive={directive}
              onStart={start}
              onPause={pause}
              onAdvance={advance}
              onCompleteStep={completeStep}
              onFailStep={failStep}
              onSkipStep={skipStep}
              onUpdateGoal={updateGoal}
              onUpdate={update}
              onDelete={handleDelete}
              onRefresh={refreshDetail}
              onCleanup={cleanup}
              onPickUpOrders={pickUpOrders}
              onCreatePR={createPR}
              dogs={dogs}
              dogsLoading={dogsLoading}
              onCreateDog={createDog}
              onUpdateDog={updateDog}
              onDeleteDog={removeDog}
              onPickUpDogOrders={pickUpDogOrders}
            />
          ) : (
            <div className="flex-1 flex items-center justify-center h-full">
              <p className="text-[#556677] font-mono text-[12px]">
                {listLoading
                  ? "Loading..."
                  : "Select a directive or create a new one"}
              </p>
            </div>
          )}
        </div>
      </main>
    </div>
  );
}