diff options
| author | soryu <soryu@soryu.co> | 2026-02-07 01:11:26 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-07 01:11:26 +0000 |
| commit | 9e9f18884c78c21f5785908fb7ccd00e2fa5436b (patch) | |
| tree | f2ca7c2a3db5350186282ae0be0e539aa77c0320 /makima/frontend/src | |
| parent | b8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56 (diff) | |
| download | soryu-9e9f18884c78c21f5785908fb7ccd00e2fa5436b.tar.gz soryu-9e9f18884c78c21f5785908fb7ccd00e2fa5436b.zip | |
Add new directive initial implementation
Diffstat (limited to 'makima/frontend/src')
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 1 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 200 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveList.tsx | 135 | ||||
| -rw-r--r-- | makima/frontend/src/hooks/useDirectives.ts | 108 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 158 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 17 | ||||
| -rw-r--r-- | makima/frontend/src/routes/directives.tsx | 209 |
7 files changed, 828 insertions, 0 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index fb95c7f..f7e67db 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -11,6 +11,7 @@ interface NavLink { const NAV_LINKS: NavLink[] = [ { label: "Listen", href: "/listen" }, { label: "Contracts", href: "/contracts", requiresAuth: true }, + { label: "Directives", href: "/directives", requiresAuth: true }, { label: "Board", href: "/workflow", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, { label: "History", href: "/history", requiresAuth: true }, diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx new file mode 100644 index 0000000..3634a79 --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -0,0 +1,200 @@ +import type { + DirectiveWithChains, + DirectiveStatus, + DirectiveChain, +} from "../../lib/api"; + +interface DirectiveDetailProps { + directive: DirectiveWithChains; + onBack: () => void; + onDelete?: (id: string) => 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", +}; + +function ChainCard({ chain }: { chain: DirectiveChain }) { + return ( + <div className="p-3 border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)]"> + <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} · {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> + ); +} + +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, +}: DirectiveDetailProps) { + 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" + > + ← Back + </button> + <span + className={`font-mono text-[10px] uppercase ${ + statusColors[directive.status as DirectiveStatus] || "text-[#888]" + }`} + > + {directive.status} + </span> + <span className="font-mono text-[10px] text-[#7788aa]"> + v{directive.version} + </span> + {onDelete && ( + <button + onClick={() => onDelete(directive.id)} + className="ml-auto font-mono text-[10px] text-red-400 hover:text-red-300 transition-colors uppercase" + > + Delete + </button> + )} + </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"> + {/* 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]"> + No chains yet. Chains are created during planning. + </p> + ) : ( + <div className="space-y-2"> + {directive.chains.map((chain) => ( + <ChainCard key={chain.id} chain={chain} /> + ))} + </div> + )} + </div> + </div> + </div> + ); +} diff --git a/makima/frontend/src/components/directives/DirectiveList.tsx b/makima/frontend/src/components/directives/DirectiveList.tsx new file mode 100644 index 0000000..a900b7b --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveList.tsx @@ -0,0 +1,135 @@ +import { useState } from "react"; +import type { DirectiveSummary, DirectiveStatus } from "../../lib/api"; + +interface DirectiveListProps { + directives: DirectiveSummary[]; + loading: boolean; + onSelect: (id: string) => void; + onCreate: () => void; + onDelete?: (directive: DirectiveSummary) => void; + selectedId?: string; +} + +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", +}; + +export function DirectiveList({ + directives, + loading, + onSelect, + onCreate, + onDelete, + selectedId, +}: DirectiveListProps) { + const [filter, setFilter] = useState<DirectiveStatus | "all">("all"); + + const filteredDirectives = + filter === "all" + ? directives + : directives.filter((d) => d.status === filter); + + if (loading) { + return ( + <div className="panel h-full flex items-center justify-center"> + <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div> + </div> + ); + } + + 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"> + <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider"> + Directives + </h2> + <button + onClick={onCreate} + className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" + > + + New + </button> + </div> + {/* Filter tabs */} + <div className="flex gap-1 flex-wrap"> + {(["all", "draft", "planning", "active", "paused", "completed", "failed"] as const).map( + (status) => ( + <button + key={status} + onClick={() => setFilter(status)} + className={`px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider border transition-colors ${ + filter === status + ? "bg-[#0f3c78] border-[#3f6fb3] text-[#dbe7ff]" + : "bg-transparent border-[rgba(117,170,252,0.2)] text-[#7788aa] hover:border-[rgba(117,170,252,0.4)]" + }`} + > + {status} + </button> + ) + )} + </div> + </div> + + {/* List */} + <div className="flex-1 overflow-y-auto"> + {filteredDirectives.length === 0 ? ( + <div className="p-4 text-center"> + <p className="font-mono text-sm text-[#7788aa]"> + {directives.length === 0 + ? "No directives yet" + : "No matching directives"} + </p> + </div> + ) : ( + filteredDirectives.map((directive) => ( + <div + key={directive.id} + onClick={() => onSelect(directive.id)} + onContextMenu={(e) => { + if (onDelete) { + e.preventDefault(); + } + }} + className={`p-3 border-b border-dashed border-[rgba(117,170,252,0.15)] cursor-pointer transition-colors hover:bg-[rgba(117,170,252,0.05)] ${ + selectedId === directive.id + ? "bg-[rgba(117,170,252,0.1)]" + : "" + }`} + > + <div className="flex items-start justify-between gap-2"> + <div className="flex-1 min-w-0"> + <div className="font-mono text-sm text-[#dbe7ff] truncate"> + {directive.title} + </div> + <div className="font-mono text-xs text-[#7788aa] mt-0.5 line-clamp-2"> + {directive.goal} + </div> + </div> + <div className="flex flex-col items-end gap-1 shrink-0"> + <span + className={`font-mono text-[10px] uppercase ${ + statusColors[directive.status] || "text-[#888]" + }`} + > + {directive.status} + </span> + <span className="font-mono text-[10px] text-[#7788aa]"> + {directive.chainCount}ch / {directive.stepCount}st + </span> + </div> + </div> + </div> + )) + )} + </div> + </div> + ); +} diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts new file mode 100644 index 0000000..001cf89 --- /dev/null +++ b/makima/frontend/src/hooks/useDirectives.ts @@ -0,0 +1,108 @@ +import { useState, useCallback, useEffect } from "react"; +import { + listDirectives, + getDirective, + createDirective, + updateDirective, + deleteDirective, + type DirectiveSummary, + type DirectiveWithChains, + type CreateDirectiveRequest, + type UpdateDirectiveRequest, +} from "../lib/api"; + +export function useDirectives() { + const [directives, setDirectives] = useState<DirectiveSummary[]>([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + const fetchDirectives = useCallback(async () => { + setLoading(true); + setError(null); + try { + const response = await listDirectives(); + setDirectives(response.directives); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to fetch directives"); + } finally { + setLoading(false); + } + }, []); + + const fetchDirective = useCallback( + async (id: string): Promise<DirectiveWithChains | null> => { + setError(null); + try { + return await getDirective(id); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to fetch directive"); + return null; + } + }, + [] + ); + + const saveDirective = useCallback( + async (data: CreateDirectiveRequest): Promise<DirectiveSummary | null> => { + setError(null); + try { + const directive = await createDirective(data); + await fetchDirectives(); + return directive as unknown as DirectiveSummary; + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to create directive"); + return null; + } + }, + [fetchDirectives] + ); + + const editDirective = useCallback( + async ( + id: string, + data: UpdateDirectiveRequest + ): Promise<DirectiveSummary | null> => { + setError(null); + try { + const directive = await updateDirective(id, data); + await fetchDirectives(); + return directive as unknown as DirectiveSummary; + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to update directive"); + return null; + } + }, + [fetchDirectives] + ); + + const removeDirective = useCallback( + async (id: string): Promise<boolean> => { + setError(null); + try { + await deleteDirective(id); + await fetchDirectives(); + return true; + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to delete directive"); + return false; + } + }, + [fetchDirectives] + ); + + // Initial fetch + useEffect(() => { + fetchDirectives(); + }, [fetchDirectives]); + + return { + directives, + loading, + error, + fetchDirectives, + fetchDirective, + saveDirective, + editDirective, + removeDirective, + }; +} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 9f5ff88..6c450eb 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3003,3 +3003,161 @@ export async function listTaskPatches(taskId: string, contractId: string): Promi return res.json(); } +// ============================================================================= +// Directive Types & API +// ============================================================================= + +export type DirectiveStatus = "draft" | "planning" | "active" | "paused" | "completed" | "archived" | "failed"; +export type AutonomyLevel = "full_auto" | "guardrails" | "manual"; + +export interface DirectiveSummary { + id: string; + title: string; + goal: string; + status: DirectiveStatus; + autonomyLevel: AutonomyLevel; + chainCount: number; + stepCount: number; + totalCostUsd: number; + version: number; + createdAt: string; + updatedAt: string; +} + +export interface Directive { + id: string; + ownerId: string; + title: string; + goal: string; + requirements: unknown[]; + acceptanceCriteria: unknown[]; + constraints: unknown[]; + externalDependencies: unknown[]; + status: DirectiveStatus; + autonomyLevel: AutonomyLevel; + confidenceThresholdGreen: number; + confidenceThresholdYellow: number; + maxTotalCostUsd: number | null; + maxWallTimeMinutes: number | null; + maxReworkCycles: number | null; + maxChainRegenerations: number | null; + repositoryUrl: string | null; + localPath: string | null; + baseBranch: string | null; + orchestratorContractId: string | null; + currentChainId: string | null; + chainGenerationCount: number; + totalCostUsd: number; + startedAt: string | null; + completedAt: string | null; + version: number; + createdAt: string; + updatedAt: string; +} + +export interface DirectiveChain { + id: string; + directiveId: string; + generation: number; + name: string; + description: string | null; + rationale: string | null; + planningModel: string | null; + status: string; + totalSteps: number; + completedSteps: number; + failedSteps: number; + currentConfidence: number | null; + startedAt: string | null; + completedAt: string | null; + version: number; + createdAt: string; + updatedAt: string; +} + +export interface DirectiveWithChains extends Directive { + chains: DirectiveChain[]; +} + +export interface DirectiveListResponse { + directives: DirectiveSummary[]; + total: number; +} + +export interface CreateDirectiveRequest { + title: string; + goal: string; + requirements?: unknown[]; + acceptanceCriteria?: unknown[]; + constraints?: unknown[]; + externalDependencies?: unknown[]; + autonomyLevel?: AutonomyLevel; + repositoryUrl?: string; + localPath?: string; + baseBranch?: string; +} + +export interface UpdateDirectiveRequest { + title?: string; + goal?: string; + status?: DirectiveStatus; + autonomyLevel?: AutonomyLevel; + version?: number; +} + +export async function listDirectives(): Promise<DirectiveListResponse> { + const res = await authFetch(`${API_BASE}/api/v1/directives`); + if (!res.ok) { + throw new Error(`Failed to list directives: ${res.statusText}`); + } + return res.json(); +} + +export async function getDirective(id: string): Promise<DirectiveWithChains> { + const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`); + if (!res.ok) { + throw new Error(`Failed to get directive: ${res.statusText}`); + } + return res.json(); +} + +export async function createDirective( + data: CreateDirectiveRequest +): Promise<Directive> { + const res = await authFetch(`${API_BASE}/api/v1/directives`, { + method: "POST", + body: JSON.stringify(data), + }); + if (!res.ok) { + throw new Error(`Failed to create directive: ${res.statusText}`); + } + return res.json(); +} + +export async function updateDirective( + id: string, + data: UpdateDirectiveRequest +): Promise<Directive> { + const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { + method: "PUT", + body: JSON.stringify(data), + }); + if (res.status === 409) { + const conflict = (await res.json()) as ConflictErrorResponse; + throw new VersionConflictError(conflict); + } + if (!res.ok) { + throw new Error(`Failed to update directive: ${res.statusText}`); + } + return res.json(); +} + +export async function deleteDirective(id: string): Promise<void> { + const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { + method: "DELETE", + }); + if (!res.ok) { + throw new Error(`Failed to delete directive: ${res.statusText}`); + } +} + diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index 50fffe4..f07a143 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -18,6 +18,7 @@ import HistoryPage from "./routes/history"; import LoginPage from "./routes/login"; import SettingsPage from "./routes/settings"; import ContractFilePage from "./routes/contract-file"; +import DirectivesPage from "./routes/directives"; import SpeakPage from "./routes/speak"; createRoot(document.getElementById("root")!).render( @@ -80,6 +81,22 @@ createRoot(document.getElementById("root")!).render( } /> <Route + path="/directives" + element={ + <ProtectedRoute> + <DirectivesPage /> + </ProtectedRoute> + } + /> + <Route + path="/directives/:id" + element={ + <ProtectedRoute> + <DirectivesPage /> + </ProtectedRoute> + } + /> + <Route path="/workflow" element={ <ProtectedRoute> diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx new file mode 100644 index 0000000..89535e2 --- /dev/null +++ b/makima/frontend/src/routes/directives.tsx @@ -0,0 +1,209 @@ +import { useState, useCallback, useEffect } 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 } from "../hooks/useDirectives"; +import { useAuth } from "../contexts/AuthContext"; +import type { + DirectiveWithChains, + CreateDirectiveRequest, +} from "../lib/api"; + +export default function DirectivesPage() { + const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); + const navigate = useNavigate(); + + // Redirect to login if not authenticated (when auth is configured) + 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 /> + <DirectivesContent /> + </div> + ); +} + +function DirectivesContent() { + const { id } = useParams<{ id?: string }>(); + const navigate = useNavigate(); + const { + directives, + loading, + error, + fetchDirective, + saveDirective, + removeDirective, + } = useDirectives(); + + const [selectedDirective, setSelectedDirective] = + useState<DirectiveWithChains | null>(null); + const [detailLoading, setDetailLoading] = useState(false); + const [showCreateForm, setShowCreateForm] = useState(false); + const [createTitle, setCreateTitle] = useState(""); + const [createGoal, setCreateGoal] = useState(""); + const [createRepoUrl, setCreateRepoUrl] = useState(""); + + // Load directive when ID changes + useEffect(() => { + if (id) { + setDetailLoading(true); + fetchDirective(id).then((d) => { + setSelectedDirective(d); + setDetailLoading(false); + }); + } else { + setSelectedDirective(null); + } + }, [id, fetchDirective]); + + const handleSelect = useCallback( + (directiveId: string) => { + navigate(`/directives/${directiveId}`); + }, + [navigate] + ); + + const handleBack = useCallback(() => { + navigate("/directives"); + }, [navigate]); + + const handleCreate = useCallback(async () => { + if (!createTitle.trim() || !createGoal.trim()) return; + + const data: CreateDirectiveRequest = { + title: createTitle.trim(), + goal: createGoal.trim(), + }; + if (createRepoUrl.trim()) { + data.repositoryUrl = createRepoUrl.trim(); + } + + const result = await saveDirective(data); + if (result) { + setShowCreateForm(false); + setCreateTitle(""); + setCreateGoal(""); + setCreateRepoUrl(""); + } + }, [createTitle, createGoal, createRepoUrl, saveDirective]); + + const handleDelete = useCallback( + async (directiveId: string) => { + const ok = await removeDirective(directiveId); + if (ok && id === directiveId) { + navigate("/directives"); + } + }, + [removeDirective, id, navigate] + ); + + // Detail view + if (id) { + if (detailLoading) { + return ( + <main className="flex-1 flex items-center justify-center"> + <p className="text-[#7788aa] font-mono text-sm">Loading directive...</p> + </main> + ); + } + if (!selectedDirective) { + return ( + <main className="flex-1 flex items-center justify-center"> + <p className="text-[#7788aa] font-mono text-sm">Directive not found</p> + </main> + ); + } + return ( + <main className="flex-1 p-4 max-w-4xl mx-auto w-full"> + <DirectiveDetail + directive={selectedDirective} + onBack={handleBack} + onDelete={handleDelete} + /> + </main> + ); + } + + // List view + return ( + <main className="flex-1 p-4 max-w-4xl mx-auto w-full"> + {error && ( + <div className="mb-4 p-2 border border-red-400/30 bg-red-400/5 font-mono text-xs text-red-400"> + {error} + </div> + )} + + {showCreateForm && ( + <div className="mb-4 p-4 border border-dashed border-[rgba(117,170,252,0.35)] bg-[rgba(117,170,252,0.03)]"> + <h3 className="font-mono text-xs text-[#75aafc] uppercase tracking-wider mb-3"> + New Directive + </h3> + <div className="space-y-2"> + <input + type="text" + placeholder="Title" + value={createTitle} + onChange={(e) => setCreateTitle(e.target.value)} + className="w-full px-2 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0a1628] border border-[rgba(117,170,252,0.3)] focus:border-[#75aafc] outline-none" + /> + <textarea + placeholder="Goal - what should be accomplished?" + value={createGoal} + onChange={(e) => setCreateGoal(e.target.value)} + rows={3} + className="w-full px-2 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0a1628] border border-[rgba(117,170,252,0.3)] focus:border-[#75aafc] outline-none resize-none" + /> + <input + type="text" + placeholder="Repository URL (optional)" + value={createRepoUrl} + onChange={(e) => setCreateRepoUrl(e.target.value)} + className="w-full px-2 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0a1628] border border-[rgba(117,170,252,0.3)] focus:border-[#75aafc] outline-none" + /> + <div className="flex gap-2"> + <button + onClick={handleCreate} + disabled={!createTitle.trim() || !createGoal.trim()} + className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase disabled:opacity-50 disabled:cursor-not-allowed" + > + Create + </button> + <button + onClick={() => setShowCreateForm(false)} + className="px-3 py-1.5 font-mono text-xs text-[#7788aa] hover:text-[#dbe7ff] transition-colors uppercase" + > + Cancel + </button> + </div> + </div> + </div> + )} + + <DirectiveList + directives={directives} + loading={loading} + onSelect={handleSelect} + onCreate={() => setShowCreateForm(true)} + onDelete={(d) => handleDelete(d.id)} + selectedId={id} + /> + </main> + ); +} |
