diff options
| author | soryu <soryu@soryu.co> | 2026-01-11 05:52:14 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 00:21:16 +0000 |
| commit | 87044a747b47bd83249d61a45842c7f7b2eae56d (patch) | |
| tree | ef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/components/contracts/TaskDerivationPreview.tsx | |
| parent | 077820c4167c168072d217a1b01df840463a12a8 (diff) | |
| download | soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip | |
Contract system
Diffstat (limited to 'makima/frontend/src/components/contracts/TaskDerivationPreview.tsx')
| -rw-r--r-- | makima/frontend/src/components/contracts/TaskDerivationPreview.tsx | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/makima/frontend/src/components/contracts/TaskDerivationPreview.tsx b/makima/frontend/src/components/contracts/TaskDerivationPreview.tsx new file mode 100644 index 0000000..07421ef --- /dev/null +++ b/makima/frontend/src/components/contracts/TaskDerivationPreview.tsx @@ -0,0 +1,221 @@ +import { useState, useCallback } from "react"; + +export interface ParsedTask { + name: string; + description?: string; + group?: string; + order: number; + completed: boolean; + dependencies: string[]; +} + +interface TaskDerivationPreviewProps { + tasks: ParsedTask[]; + groups: string[]; + fileName: string; + onCreateTasks: (selectedTasks: ParsedTask[]) => void; + onCancel: () => void; + loading?: boolean; +} + +export function TaskDerivationPreview({ + tasks, + groups, + fileName, + onCreateTasks, + onCancel, + loading = false, +}: TaskDerivationPreviewProps) { + const [selectedIndices, setSelectedIndices] = useState<Set<number>>( + new Set(tasks.map((_, i) => i)) // Select all by default + ); + + const toggleTask = useCallback((index: number) => { + setSelectedIndices((prev) => { + const newSet = new Set(prev); + if (newSet.has(index)) { + newSet.delete(index); + } else { + newSet.add(index); + } + return newSet; + }); + }, []); + + const selectAll = useCallback(() => { + setSelectedIndices(new Set(tasks.map((_, i) => i))); + }, [tasks]); + + const selectNone = useCallback(() => { + setSelectedIndices(new Set()); + }, []); + + const handleCreate = useCallback(() => { + const selectedTasks = tasks.filter((_, i) => selectedIndices.has(i)); + onCreateTasks(selectedTasks); + }, [tasks, selectedIndices, onCreateTasks]); + + // Group tasks by their group property + const tasksByGroup = tasks.reduce((acc, task, index) => { + const groupKey = task.group || "Ungrouped"; + if (!acc[groupKey]) { + acc[groupKey] = []; + } + acc[groupKey].push({ task, index }); + return acc; + }, {} as Record<string, { task: ParsedTask; index: number }[]>); + + const selectedCount = selectedIndices.size; + const totalCount = tasks.length; + + return ( + <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> + <div className="w-full max-w-2xl p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[80vh] flex flex-col"> + {/* Header */} + <div className="flex items-center justify-between mb-4"> + <div> + <h3 className="font-mono text-sm text-[#75aafc] uppercase"> + Create Tasks from Document + </h3> + <p className="font-mono text-xs text-[#555] mt-1"> + Source: {fileName} + </p> + </div> + <div className="flex items-center gap-2"> + <button + onClick={selectAll} + className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors" + > + Select All + </button> + <span className="text-[#555]">|</span> + <button + onClick={selectNone} + className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors" + > + Select None + </button> + </div> + </div> + + {/* Task List */} + <div className="flex-1 overflow-y-auto space-y-4 mb-4"> + {groups.length > 0 ? ( + // Grouped view + Object.entries(tasksByGroup).map(([groupName, groupTasks]) => ( + <div key={groupName} className="space-y-2"> + <h4 className="font-mono text-xs text-[#9bc3ff] uppercase border-b border-[rgba(117,170,252,0.2)] pb-1"> + {groupName} + </h4> + {groupTasks.map(({ task, index }) => ( + <TaskItem + key={index} + task={task} + index={index} + selected={selectedIndices.has(index)} + onToggle={() => toggleTask(index)} + /> + ))} + </div> + )) + ) : ( + // Flat view + tasks.map((task, index) => ( + <TaskItem + key={index} + task={task} + index={index} + selected={selectedIndices.has(index)} + onToggle={() => toggleTask(index)} + /> + )) + )} + </div> + + {/* Footer */} + <div className="flex items-center justify-between pt-4 border-t border-[rgba(117,170,252,0.2)]"> + <span className="font-mono text-xs text-[#555]"> + {selectedCount} of {totalCount} tasks selected + </span> + <div className="flex gap-2"> + <button + onClick={onCancel} + disabled={loading} + className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors disabled:opacity-50" + > + Cancel + </button> + <button + onClick={handleCreate} + disabled={loading || selectedCount === 0} + className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + > + {loading ? "Creating..." : `Create ${selectedCount} Task${selectedCount !== 1 ? "s" : ""}`} + </button> + </div> + </div> + + {/* Chaining info */} + {selectedCount > 1 && ( + <p className="font-mono text-[10px] text-[#555] mt-2 text-center"> + Tasks will be chained: each task continues from the previous one's work + </p> + )} + </div> + </div> + ); +} + +function TaskItem({ + task, + index, + selected, + onToggle, +}: { + task: ParsedTask; + index: number; + selected: boolean; + onToggle: () => void; +}) { + return ( + <button + onClick={onToggle} + className={`w-full text-left p-3 border transition-colors ${ + selected + ? "border-[#75aafc] bg-[rgba(117,170,252,0.1)]" + : "border-[rgba(117,170,252,0.15)] hover:border-[rgba(117,170,252,0.3)]" + }`} + > + <div className="flex items-start gap-2"> + <span + className={`font-mono text-xs mt-0.5 ${ + selected ? "text-[#75aafc]" : "text-[#555]" + }`} + > + {selected ? "[x]" : "[ ]"} + </span> + <div className="flex-1 min-w-0"> + <div className="flex items-center gap-2"> + <span className="font-mono text-[10px] text-[#555]">#{index + 1}</span> + <span className="font-mono text-sm text-[#dbe7ff]">{task.name}</span> + {task.completed && ( + <span className="font-mono text-[9px] text-green-400 uppercase"> + done in source + </span> + )} + </div> + {task.description && ( + <p className="font-mono text-xs text-[#555] mt-1 truncate"> + {task.description} + </p> + )} + {task.dependencies.length > 0 && ( + <p className="font-mono text-[10px] text-[#75aafc] mt-1"> + Depends on: {task.dependencies.join(", ")} + </p> + )} + </div> + </div> + </button> + ); +} |
