summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/contracts/TaskDerivationPreview.tsx
blob: 07421ef00c3dc49e647c79bd2996c4abf2be7d47 (plain) (tree)




























































































































































































































                                                                                                                                                                                              
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>
  );
}