diff options
Diffstat (limited to 'makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx')
| -rw-r--r-- | makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx b/makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx new file mode 100644 index 0000000..3621b08 --- /dev/null +++ b/makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx @@ -0,0 +1,262 @@ +import { useState, useCallback, useEffect } from "react"; +import type { TaskWithSubtasks, TaskStatus } from "../../lib/api"; +import { getTask, updateTask } from "../../lib/api"; + +interface InlineSubtaskEditorProps { + subtaskId: string; + onClose: () => void; + onUpdated: () => void; + onNavigate?: (taskId: string) => void; +} + +function getStatusColor(status: TaskStatus): string { + switch (status) { + case "pending": + return "text-[#9bc3ff]"; + case "running": + return "text-green-400"; + case "paused": + return "text-yellow-400"; + case "blocked": + return "text-orange-400"; + case "done": + return "text-emerald-400"; + case "failed": + return "text-red-400"; + case "merged": + return "text-purple-400"; + default: + return "text-[#9bc3ff]"; + } +} + +function getStatusBgColor(status: TaskStatus): string { + switch (status) { + case "pending": + return "bg-[rgba(117,170,252,0.1)]"; + case "running": + return "bg-green-400/10"; + case "paused": + return "bg-yellow-400/10"; + case "blocked": + return "bg-orange-400/10"; + case "done": + return "bg-emerald-400/10"; + case "failed": + return "bg-red-400/10"; + case "merged": + return "bg-purple-400/10"; + default: + return "bg-[rgba(117,170,252,0.1)]"; + } +} + +export function InlineSubtaskEditor({ + subtaskId, + onClose, + onUpdated, + onNavigate, +}: InlineSubtaskEditorProps) { + const [subtask, setSubtask] = useState<TaskWithSubtasks | null>(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editName, setEditName] = useState(""); + const [editDescription, setEditDescription] = useState(""); + const [editPlan, setEditPlan] = useState(""); + + // Load subtask details + useEffect(() => { + setLoading(true); + getTask(subtaskId) + .then((task) => { + setSubtask(task); + setEditName(task.name); + setEditDescription(task.description || ""); + setEditPlan(task.plan); + }) + .catch((err) => { + console.error("Failed to load subtask:", err); + }) + .finally(() => { + setLoading(false); + }); + }, [subtaskId]); + + const handleSave = useCallback(async () => { + if (!subtask || saving) return; + setSaving(true); + try { + await updateTask(subtaskId, { + name: editName, + description: editDescription || undefined, + plan: editPlan, + version: subtask.version, + }); + // Refresh subtask + const updated = await getTask(subtaskId); + setSubtask(updated); + setIsEditing(false); + onUpdated(); + } catch (err) { + console.error("Failed to save subtask:", err); + } finally { + setSaving(false); + } + }, [subtask, subtaskId, editName, editDescription, editPlan, saving, onUpdated]); + + const handleCancel = useCallback(() => { + if (subtask) { + setEditName(subtask.name); + setEditDescription(subtask.description || ""); + setEditPlan(subtask.plan); + } + setIsEditing(false); + }, [subtask]); + + if (loading) { + return ( + <div className="p-4 bg-[rgba(0,0,0,0.2)] border-l-2 border-[#3f6fb3]"> + <div className="font-mono text-xs text-[#75aafc]">Loading subtask...</div> + </div> + ); + } + + if (!subtask) { + return ( + <div className="p-4 bg-[rgba(0,0,0,0.2)] border-l-2 border-red-400"> + <div className="font-mono text-xs text-red-400">Failed to load subtask</div> + </div> + ); + } + + return ( + <div className="bg-[rgba(0,0,0,0.2)] border-l-2 border-[#3f6fb3]"> + {/* Header */} + <div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.15)]"> + <div className="flex items-center gap-2"> + <button + onClick={onClose} + className="font-mono text-[10px] text-[#555] hover:text-[#75aafc]" + > + [close] + </button> + <span + className={`px-1.5 py-0.5 font-mono text-[9px] uppercase ${getStatusColor( + subtask.status as TaskStatus + )} ${getStatusBgColor(subtask.status as TaskStatus)} border border-current/20`} + > + {subtask.status} + </span> + {onNavigate && ( + <button + onClick={() => onNavigate(subtaskId)} + className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff]" + > + [open full view] + </button> + )} + </div> + <div className="flex items-center gap-2"> + {isEditing ? ( + <> + <button + onClick={handleCancel} + disabled={saving} + className="px-2 py-0.5 font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] disabled:opacity-50" + > + Cancel + </button> + <button + onClick={handleSave} + disabled={saving} + className="px-2 py-0.5 font-mono text-[10px] text-green-400 border border-green-400/30 hover:border-green-400/50 disabled:opacity-50" + > + {saving ? "..." : "Save"} + </button> + </> + ) : ( + <button + onClick={() => setIsEditing(true)} + className="px-2 py-0.5 font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff]" + > + Edit + </button> + )} + </div> + </div> + + {/* Content */} + <div className="p-3 space-y-3"> + {/* Name */} + {isEditing ? ( + <input + type="text" + value={editName} + onChange={(e) => setEditName(e.target.value)} + className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-sm px-2 py-1 outline-none focus:border-[#3f6fb3]" + placeholder="Subtask name" + /> + ) : ( + <div className="font-mono text-sm text-[#dbe7ff]">{subtask.name}</div> + )} + + {/* Description */} + {isEditing ? ( + <textarea + value={editDescription} + onChange={(e) => setEditDescription(e.target.value)} + className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] min-h-[40px] resize-y" + placeholder="Description (optional)" + /> + ) : subtask.description ? ( + <div className="font-mono text-xs text-[#75aafc]">{subtask.description}</div> + ) : null} + + {/* Plan */} + <div className="space-y-1"> + <div className="font-mono text-[10px] text-[#555] uppercase">Plan</div> + {isEditing ? ( + <textarea + value={editPlan} + onChange={(e) => setEditPlan(e.target.value)} + className="w-full bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] min-h-[100px] resize-y" + placeholder="Plan/instructions..." + /> + ) : ( + <pre className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-2 font-mono text-xs text-[#9bc3ff] whitespace-pre-wrap max-h-[150px] overflow-y-auto"> + {subtask.plan} + </pre> + )} + </div> + + {/* Progress/Error */} + {subtask.progressSummary && ( + <div className="font-mono text-[10px] text-[#75aafc]"> + <span className="text-[#555]">Progress:</span> {subtask.progressSummary} + </div> + )} + {subtask.errorMessage && ( + <div className="font-mono text-[10px] text-red-400"> + <span className="text-red-400/50">Error:</span> {subtask.errorMessage} + </div> + )} + + {/* Nested subtasks indicator */} + {subtask.subtasks.length > 0 && ( + <div className="font-mono text-[10px] text-[#555]"> + Has {subtask.subtasks.length} subtask{subtask.subtasks.length > 1 ? "s" : ""} + {onNavigate && ( + <button + onClick={() => onNavigate(subtaskId)} + className="ml-2 text-[#75aafc] hover:text-[#9bc3ff]" + > + [view all] + </button> + )} + </div> + )} + </div> + </div> + ); +} |
