import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { Masthead } from "../components/Masthead";
import { useDirective, useDirectives } from "../hooks/useDirectives";
import { useAuth } from "../contexts/AuthContext";
import { DocumentEditor } from "../components/directives/DocumentEditor";
import type { DirectiveSummary, DirectiveStatus } from "../lib/api";
// Status dot color, matching the existing tabular UI's badge palette so the
// document mode feels like a sibling of the existing list, not a foreign UI.
const STATUS_DOT: Record<DirectiveStatus, string> = {
draft: "bg-[#556677]",
active: "bg-green-400",
idle: "bg-yellow-400",
paused: "bg-orange-400",
archived: "bg-[#3a4a6a]",
};
// =============================================================================
// Sidebar grouping — group directives by lifecycle stage so the file tree
// reads like a folder per status. We collapse the noisy ones (Archived) by
// default and keep Active / Idle expanded.
// =============================================================================
type SidebarGroup = "active" | "idle" | "archived";
const GROUP_LABEL: Record<SidebarGroup, string> = {
active: "active",
idle: "idle",
archived: "archived",
};
function bucketOf(status: DirectiveStatus): SidebarGroup {
if (status === "active" || status === "paused") return "active";
if (status === "archived") return "archived";
// draft + idle land in the idle bucket (i.e. "not currently running").
return "idle";
}
// =============================================================================
// Sidebar icons (inline SVG, no new deps)
// =============================================================================
function FolderIcon({ open = false }: { open?: boolean }) {
return (
<svg
viewBox="0 0 16 16"
width={12}
height={12}
className="shrink-0"
aria-hidden
>
{open ? (
<path
d="M1.5 3.5a1 1 0 0 1 1-1h3.382a1 1 0 0 1 .894.553l.448.894a1 1 0 0 0 .894.553H13.5a1 1 0 0 1 1 1V6H1.5V3.5z M1 6.5h13.382a.5.5 0 0 1 .49.598l-.9 5A.5.5 0 0 1 13.482 12.5H2.518a.5.5 0 0 1-.49-.402l-.9-5A.5.5 0 0 1 1.62 6.5H1z"
fill="#75aafc"
opacity="0.85"
/>
) : (
<path
d="M1.5 4a1 1 0 0 1 1-1h3.382a1 1 0 0 1 .894.553l.448.894a1 1 0 0 0 .894.553H13.5a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V4z"
fill="#75aafc"
opacity="0.65"
/>
)}
</svg>
);
}
function FileIcon() {
return (
<svg
viewBox="0 0 16 16"
width={12}
height={12}
className="shrink-0"
aria-hidden
>
<path
d="M3 1.5h6.293a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 13.293 5.5H13V14a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V1.5z"
fill="none"
stroke="#9bc3ff"
strokeWidth="1"
/>
<path
d="M9.5 1.5v3h3"
fill="none"
stroke="#9bc3ff"
strokeWidth="1"
/>
</svg>
);
}
function Caret({ open }: { open: boolean }) {
return (
<svg
viewBox="0 0 8 8"
width={8}
height={8}
className={`shrink-0 transition-transform ${open ? "rotate-90" : ""}`}
aria-hidden
>
<path d="M2 1l4 3-4 3z" fill="#7788aa" />
</svg>
);
}
// =============================================================================
// Sidebar
// =============================================================================
interface SidebarProps {
directives: DirectiveSummary[];
loading: boolean;
selectedId: string | null;
onSelect: (id: string) => void;
}
function DocumentSidebar({ directives, loading, selectedId, onSelect }: SidebarProps) {
const groups: Record<SidebarGroup, DirectiveSummary[]> = useMemo(() => {
const out: Record<SidebarGroup, DirectiveSummary[]> = {
active: [],
idle: [],
archived: [],
};
for (const d of directives) {
out[bucketOf(d.status)].push(d);
}
// Sort each group alphabetically so it feels like a stable file tree.
(Object.keys(out) as SidebarGroup[]).forEach((k) => {
out[k].sort((a, b) =>
a.title.localeCompare(b.title, undefined, { sensitivity: "base" }),
);
});
return out;
}, [directives]);
// Default-collapsed state per folder. Archived is collapsed by default
// (it's history); the other two are open so users see their work.
const [openGroups, setOpenGroups] = useState<Record<SidebarGroup, boolean>>({
active: true,
idle: true,
archived: false,
});
const toggleGroup = (g: SidebarGroup) =>
setOpenGroups((prev) => ({ ...prev, [g]: !prev[g] }));
return (
<div className="flex flex-col h-full">
{/* Sidebar header */}
<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">
Documents
</span>
<span className="text-[10px] font-mono text-[#556677]">
{directives.length}
</span>
</div>
{/* Top-level "directives/" folder */}
<div className="flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-mono text-[#9bc3ff]">
<FolderIcon open />
<span>directives/</span>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto pb-4">
{loading && directives.length === 0 ? (
<div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
Loading...
</div>
) : directives.length === 0 ? (
<div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
No directives yet
</div>
) : (
(Object.keys(groups) as SidebarGroup[]).map((group) => {
const list = groups[group];
if (list.length === 0) return null;
const open = openGroups[group];
return (
<div key={group} className="select-none">
{/* Group header (sub-folder) */}
<button
type="button"
onClick={() => toggleGroup(group)}
className="w-full flex items-center gap-1.5 pl-4 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)]"
>
<Caret open={open} />
<FolderIcon open={open} />
<span>{GROUP_LABEL[group]}/</span>
<span className="ml-auto text-[10px] text-[#556677]">
{list.length}
</span>
</button>
{/* Files inside the group */}
{open && (
<ul className="py-0.5">
{list.map((d) => {
const isSelected = d.id === selectedId;
const dot = STATUS_DOT[d.status] ?? STATUS_DOT.draft;
const slug = d.title
.trim()
.replace(/\s+/g, "-")
.replace(/[^a-zA-Z0-9._-]/g, "")
.toLowerCase();
const fileName =
slug.length > 0 ? `${slug}.md` : `${d.id.slice(0, 8)}.md`;
const orchestratorRunning = !!d.orchestratorTaskId;
return (
<li key={d.id}>
<button
type="button"
onClick={() => onSelect(d.id)}
title={d.title}
className={`w-full text-left flex items-center gap-1.5 pl-9 pr-3 py-1 font-mono text-[11px] transition-colors ${
isSelected
? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]"
: "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent"
}`}
>
<FileIcon />
<span
className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`}
aria-hidden
/>
<span className="truncate flex-1">{fileName}</span>
{orchestratorRunning && (
<span
className="inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-yellow-400 animate-pulse"
title="Orchestrator running"
aria-label="Orchestrator running"
/>
)}
</button>
</li>
);
})}
</ul>
)}
</div>
);
})
)}
</div>
</div>
);
}
// =============================================================================
// Editor shell — wraps DocumentEditor and handles the "no document selected"
// and loading states.
// =============================================================================
interface EditorShellProps {
selectedId: string | undefined;
hasDirectives: boolean;
listLoading: boolean;
}
function EditorShell({ selectedId, hasDirectives, listLoading }: EditorShellProps) {
const {
directive,
loading,
updateGoal,
cleanup,
createPR,
pickUpOrders,
} = useDirective(selectedId);
if (!selectedId) {
return (
<div className="flex-1 flex items-center justify-center h-full">
<p className="text-[#556677] font-mono text-[12px]">
{listLoading
? "Loading documents..."
: hasDirectives
? "Select a document from the sidebar"
: "No documents yet — create one from the legacy UI"}
</p>
</div>
);
}
if (loading && !directive) {
return (
<div className="flex-1 flex items-center justify-center h-full">
<p className="text-[#556677] font-mono text-[12px]">Loading document...</p>
</div>
);
}
if (!directive) {
return (
<div className="flex-1 flex items-center justify-center h-full">
<p className="text-[#7788aa] font-mono text-[12px]">Document not found</p>
</div>
);
}
return (
<div className="flex-1 flex flex-col h-full overflow-hidden">
{/* Document header — breadcrumb-like, mirrors a code editor's tab bar */}
<div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
<div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">
<FileIcon />
<span>directives /</span>
<span className="text-[#9bc3ff]">{directive.id.slice(0, 8)}</span>
{!!directive.orchestratorTaskId && (
<span className="ml-2 inline-flex items-center gap-1 text-yellow-400">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse" />
orchestrator running
</span>
)}
</div>
</div>
{/* Lexical editor body */}
<DocumentEditor
directive={directive}
onUpdateGoal={async (goal) => {
await updateGoal(goal);
}}
onCleanup={async () => {
await cleanup();
}}
onCreatePR={async () => {
await createPR();
}}
onPickUpOrders={async () => {
await pickUpOrders();
}}
/>
</div>
);
}
// =============================================================================
// Page
// =============================================================================
export default function DocumentDirectivesPage() {
const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
const navigate = useNavigate();
const { id: selectedId } = useParams<{ id: string }>();
const { directives, loading: listLoading } = useDirectives();
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>
);
}
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: file-tree sidebar */}
<div className="w-[240px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col bg-[#091428]">
<DocumentSidebar
directives={directives}
loading={listLoading}
selectedId={selectedId ?? null}
onSelect={(id) => navigate(`/directives/${id}`)}
/>
</div>
{/* Right: Lexical editor */}
<EditorShell
selectedId={selectedId}
hasDirectives={directives.length > 0}
listLoading={listLoading}
/>
</main>
</div>
);
}