diff --git a/apps/web/src/pages/automate-page.tsx b/apps/web/src/pages/automate-page.tsx index 4a2b6df8..a600dd41 100644 --- a/apps/web/src/pages/automate-page.tsx +++ b/apps/web/src/pages/automate-page.tsx @@ -11,6 +11,7 @@ import { Plus, Save, Trash2, + Upload, Workflow, X, } from "lucide-react"; @@ -82,6 +83,7 @@ export function AutomatePage() { const [showAllSaved, setShowAllSaved] = useState(false); const [previewCollapsed, setPreviewCollapsed] = useState(false); const [mobileToolPaletteOpen, setMobileToolPaletteOpen] = useState(false); + const [importError, setImportError] = useState(null); const hasFile = files.length > 0; const hasProcessed = !!processedUrl; @@ -258,6 +260,71 @@ export function AutomatePage() { URL.revokeObjectURL(url); }, []); + const handleImportPipeline = useCallback(async () => { + setImportError(null); + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".json"; + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; + try { + const text = await file.text(); + const data = JSON.parse(text); + + if (data.format !== "snapotter-pipeline") { + setImportError("Not a valid SnapOtter pipeline file"); + return; + } + if (typeof data.version !== "number" || data.version > 1) { + setImportError("This pipeline was created with a newer version of SnapOtter"); + return; + } + if (!data.name || typeof data.name !== "string") { + setImportError("Pipeline file is missing a name"); + return; + } + if (!Array.isArray(data.steps) || data.steps.length === 0) { + setImportError("Pipeline file has no steps"); + return; + } + + const res = await fetch("/api/v1/pipeline/save", { + method: "POST", + headers: formatHeaders({ "Content-Type": "application/json" }), + body: JSON.stringify({ + name: data.name, + description: data.description || undefined, + steps: data.steps, + }), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: "Import failed" })); + setImportError(err.error || "Import failed"); + return; + } + + const listRes = await fetch("/api/v1/pipeline/list", { + headers: formatHeaders(), + }); + if (listRes.ok) { + const listData = await listRes.json(); + setSavedPipelines(listData.pipelines || []); + } + } catch { + setImportError("Could not read pipeline file"); + } + }; + input.click(); + }, [setSavedPipelines]); + + useEffect(() => { + if (!importError) return; + const timer = setTimeout(() => setImportError(null), 5000); + return () => clearTimeout(timer); + }, [importError]); + const handleDownloadAll = useCallback(() => { if (!batchZipBlob) return; const url = URL.createObjectURL(batchZipBlob); @@ -524,75 +591,91 @@ export function AutomatePage() { {/* Saved pipelines */} - {savedPipelines.length > 0 && ( -
-

+
+
+

Saved

- {showAllSaved ? ( -
- {savedPipelines.map((p) => ( -
- - - -
- ))} - -
- ) : ( -
- {savedPipelines.slice(0, 3).map((p) => ( + +
+ {importError && ( +
+ + {importError} +
+ )} + {savedPipelines.length === 0 ? ( +

No saved pipelines

+ ) : showAllSaved ? ( +
+ {savedPipelines.map((p) => ( +
- ))} - {savedPipelines.length > 3 && ( - )} -
- )} -
- )} + +
+ ))} + +
+ ) : ( +
+ {savedPipelines.slice(0, 3).map((p) => ( + + ))} + {savedPipelines.length > 3 && ( + + )} +
+ )} +

{/* RIGHT PANE — Pipeline Canvas + Preview */}