"use client"; import { useMemo, useState, useSyncExternalStore } from "react"; import { DndContext, closestCenter, type DragEndEvent, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { SortableContext, horizontalListSortingStrategy, arrayMove, useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { IconGripVertical, IconPlus, IconTrash, IconChevronRight, IconRosetteNumber1, } from "@tabler/icons-react"; import { StepCapabilityIcons } from "@/components/step-capability-icons"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@kandev/ui/alert-dialog"; import { ScrollArea, ScrollBar } from "@kandev/ui/scroll-area"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip"; import type { WorkflowStep } from "@/lib/types/http"; import { cn } from "@/lib/utils"; import { StepConfigPanel } from "./workflow-pipeline-editor-panels"; type WorkflowPipelineEditorProps = { steps: WorkflowStep[]; onUpdateStep: (stepId: string, updates: Partial) => void; onAddStep: () => void; onRemoveStep: (stepId: string) => void; onReorderSteps: (steps: WorkflowStep[]) => void; readOnly?: boolean; }; // --- Helpers --- function getTransitionType(step: WorkflowStep): string { const action = step.events?.on_turn_complete?.find((a) => ["move_to_next", "move_to_previous", "move_to_step"].includes(a.type), ); return action?.type ?? "none"; } function getTransitionLabel(step: WorkflowStep): string { const t = getTransitionType(step); if (t === "move_to_next") return "auto"; if (t === "move_to_previous") return "back"; if (t === "move_to_step") return "goto"; return "manual"; } // --- Pipeline Node --- type PipelineNodeProps = { step: WorkflowStep; isSelected: boolean; onSelect: () => void; onRemove: () => void; readOnly?: boolean; }; function PipelineNode({ step, isSelected, onSelect, onRemove, readOnly = false, }: PipelineNodeProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: step.id, }); const style = { transform: CSS.Transform.toString(transform), transition }; return (
{step.is_start_step && (
Start step
)}
{step.name}
manual} />
{!readOnly && ( )}
); } // --- Connector --- function PipelineConnector({ label }: { label: string }) { return (
{label}
); } // --- Pipeline Area --- type PipelineAreaProps = { steps: WorkflowStep[]; selectedStepId: string | null; onSelectStep: (stepId: string) => void; onRemoveStep: (stepId: string) => void; onAddStep: () => void; readOnly: boolean; }; function PipelineArea({ steps, selectedStepId, onSelectStep, onRemoveStep, onAddStep, readOnly, }: PipelineAreaProps) { return (
{steps.map((step, index) => (
{index > 0 && } onSelectStep(step.id)} onRemove={() => onRemoveStep(step.id)} readOnly={readOnly} />
))} {steps.length > 0 && (
)}
); } // --- Step Delete Confirmation --- function StepDeleteConfirmation({ stepName, open, onOpenChange, onConfirm, }: { stepName: string; open: boolean; onOpenChange: (open: boolean) => void; onConfirm: () => void; }) { return ( Delete step “{stepName}”? This will permanently remove the step from this workflow. This action cannot be undone. Cancel Delete ); } // --- Main Pipeline Editor --- export function WorkflowPipelineEditor({ steps, onUpdateStep, onAddStep, onRemoveStep, onReorderSteps, readOnly = false, }: WorkflowPipelineEditorProps) { const [selectedStepId, setSelectedStepId] = useState(null); const [prevStepCount, setPrevStepCount] = useState(steps.length); const [stepToConfirmDelete, setStepToConfirmDelete] = useState(null); if (steps.length !== prevStepCount) { if (steps.length > prevStepCount && steps.length > 0) setSelectedStepId(steps[steps.length - 1].id); setPrevStepCount(steps.length); } const stepItems = useMemo(() => steps.map((step) => step.id), [steps]); const isMounted = useSyncExternalStore( () => () => {}, () => true, () => false, ); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); const handleDragEnd = (event: DragEndEvent) => { if (readOnly) return; const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = steps.findIndex((step) => step.id === active.id); const newIndex = steps.findIndex((step) => step.id === over.id); if (oldIndex === -1 || newIndex === -1) return; onReorderSteps( arrayMove(steps, oldIndex, newIndex).map((step, index) => ({ ...step, position: index })), ); }; const handleSelectStep = (stepId: string) => setSelectedStepId((prev) => (prev === stepId ? null : stepId)); const requestRemoveStep = (stepId: string) => setStepToConfirmDelete(stepId); const confirmRemoveStep = () => { if (!stepToConfirmDelete) return; onRemoveStep(stepToConfirmDelete); if (selectedStepId === stepToConfirmDelete) setSelectedStepId(null); setStepToConfirmDelete(null); }; const stepToDeleteName = stepToConfirmDelete ? (steps.find((s) => s.id === stepToConfirmDelete)?.name ?? "this step") : ""; const selectedStep = steps.find((s) => s.id === selectedStepId); const pipelineArea = ( ); return (
{isMounted ? ( {pipelineArea} ) : ( pipelineArea )} {selectedStep && ( onUpdateStep(selectedStep.id, updates)} onRemove={() => requestRemoveStep(selectedStep.id)} readOnly={readOnly} /> )} { if (!open) setStepToConfirmDelete(null); }} onConfirm={confirmRemoveStep} />
); }