summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/contracts/TaskDerivationPreview.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/components/contracts/TaskDerivationPreview.tsx
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-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.tsx221
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>
+ );
+}