import { useState, useCallback } from "react"; import { Plus, X, ChevronUp, ChevronDown, ChevronRight, Play, Save, Upload, Loader2, FileImage, Download, } from "lucide-react"; import * as icons from "lucide-react"; import { TOOLS } from "@stirling-image/shared"; import { cn } from "@/lib/utils"; /** Tools that can be used as pipeline steps (excludes pipeline/batch/multi-file tools). */ const PIPELINE_TOOLS = TOOLS.filter( (t) => !["pipeline", "batch", "compare", "find-duplicates", "collage", "compose"].includes(t.id) ); export interface PipelineStep { id: string; toolId: string; settings: Record; } interface PipelineBuilderProps { steps: PipelineStep[]; onStepsChange: (steps: PipelineStep[]) => void; onSave: (name: string, description: string) => void; onExecute: (file: File) => void; saving?: boolean; executing?: boolean; executionResult?: { downloadUrl: string; originalSize: number; processedSize: number; stepsCompleted: number; } | null; } export function PipelineBuilder({ steps, onStepsChange, onSave, onExecute, saving = false, executing = false, executionResult = null, }: PipelineBuilderProps) { const [showToolPicker, setShowToolPicker] = useState(false); const [expandedStep, setExpandedStep] = useState(null); const [saveName, setSaveName] = useState(""); const [saveDescription, setSaveDescription] = useState(""); const [showSaveForm, setShowSaveForm] = useState(false); const [file, setFile] = useState(null); const addStep = useCallback( (toolId: string) => { const step: PipelineStep = { id: crypto.randomUUID(), toolId, settings: {}, }; onStepsChange([...steps, step]); setShowToolPicker(false); setExpandedStep(step.id); }, [steps, onStepsChange] ); const removeStep = useCallback( (id: string) => { onStepsChange(steps.filter((s) => s.id !== id)); if (expandedStep === id) setExpandedStep(null); }, [steps, onStepsChange, expandedStep] ); const moveStep = useCallback( (id: string, direction: "up" | "down") => { const idx = steps.findIndex((s) => s.id === id); if (idx < 0) return; const newIdx = direction === "up" ? idx - 1 : idx + 1; if (newIdx < 0 || newIdx >= steps.length) return; const newSteps = [...steps]; [newSteps[idx], newSteps[newIdx]] = [newSteps[newIdx], newSteps[idx]]; onStepsChange(newSteps); }, [steps, onStepsChange] ); const handleFileSelect = useCallback(() => { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.onchange = (e) => { const f = (e.target as HTMLInputElement).files?.[0]; if (f) setFile(f); }; input.click(); }, []); const handleFileDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); const f = e.dataTransfer.files[0]; if (f) setFile(f); }, []); const handleSave = useCallback(() => { if (!saveName.trim()) return; onSave(saveName.trim(), saveDescription.trim()); setSaveName(""); setSaveDescription(""); setShowSaveForm(false); }, [saveName, saveDescription, onSave]); const handleExecute = useCallback(() => { if (!file) return; onExecute(file); }, [file, onExecute]); const iconsMap = icons as unknown as Record< string, React.ComponentType<{ className?: string }> >; return (
{/* File Upload Area */}
e.preventDefault()} onDrop={handleFileDrop} className={cn( "rounded-xl border-2 border-dashed p-6 text-center transition-colors", file ? "border-primary/30 bg-primary/5" : "border-border bg-muted/20 hover:border-primary/30" )} > {file ? (
{file.name} ({(file.size / 1024).toFixed(0)} KB)
) : ( )}
{/* Pipeline Steps */}
{steps.length === 0 ? (
Add steps to build your automation pipeline
) : ( steps.map((step, idx) => { const tool = TOOLS.find((t) => t.id === step.toolId); if (!tool) return null; const Icon = iconsMap[tool.icon] || icons.FileImage; const isExpanded = expandedStep === step.id; return (
{/* Step number */} {idx + 1} {/* Tool icon + name */} {tool.name} {/* Controls */}
{/* Expanded settings */} {isExpanded && (

{tool.description}

Default settings will be used. Configure via the API for advanced options.

)}
); }) )}
{/* Add Step */} {showToolPicker ? (
Add a step
{PIPELINE_TOOLS.map((tool) => { const Icon = iconsMap[tool.icon] || icons.FileImage; return ( ); })}
) : ( )} {/* Execution result */} {executionResult && (
Pipeline completed ({executionResult.stepsCompleted} steps)
Original: {(executionResult.originalSize / 1024).toFixed(0)} KB Processed: {(executionResult.processedSize / 1024).toFixed(0)} KB
Download Result
)} {/* Action Buttons */}
{!showSaveForm ? ( ) : (
setSaveName(e.target.value)} placeholder="Pipeline name" className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-foreground flex-1" autoFocus /> setSaveDescription(e.target.value)} placeholder="Description (optional)" className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-foreground flex-1 hidden sm:block" />
)}
); }