diff --git a/src/components/quickstart/WorkflowBrowserView.tsx b/src/components/quickstart/WorkflowBrowserView.tsx new file mode 100644 index 00000000..ee78a98c --- /dev/null +++ b/src/components/quickstart/WorkflowBrowserView.tsx @@ -0,0 +1,322 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { WorkflowFile } from "@/store/workflowStore"; +import { QuickstartBackButton } from "./QuickstartBackButton"; +import { + getWorkflowsDirectory, + setWorkflowsDirectory, +} from "@/store/utils/localStorage"; + +interface WorkflowListEntry { + name: string; + directoryPath: string; + nodeCount: number; + lastModified: number; +} + +interface WorkflowBrowserViewProps { + onBack?: () => void; + onWorkflowLoaded: (workflow: WorkflowFile, directoryPath: string) => void; + onClose?: () => void; +} + +function formatRelativeTime(timestamp: number): string { + const seconds = Math.floor((Date.now() - timestamp) / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + const months = Math.floor(days / 30); + return `${months}mo ago`; +} + +export function WorkflowBrowserView({ + onBack, + onWorkflowLoaded, + onClose, +}: WorkflowBrowserViewProps) { + const [defaultDir, setDefaultDir] = useState(null); + const [workflows, setWorkflows] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [loadingWorkflow, setLoadingWorkflow] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + setDefaultDir(getWorkflowsDirectory()); + }, []); + + const fetchWorkflows = useCallback(async (dirPath: string) => { + setIsLoading(true); + setError(null); + try { + const res = await fetch( + `/api/list-workflows?path=${encodeURIComponent(dirPath)}` + ); + const result = await res.json(); + if (result.success) { + setWorkflows(result.workflows); + if (result.workflows.length === 0) { + setError("No workflows found in this folder"); + } + } else { + setError(result.error || "Failed to list workflows"); + } + } catch { + setError("Failed to fetch workflows"); + } finally { + setIsLoading(false); + } + }, []); + + // Fetch workflows when default directory is set + useEffect(() => { + if (defaultDir) { + fetchWorkflows(defaultDir); + } + }, [defaultDir, fetchWorkflows]); + + const handleChooseFolder = useCallback(async () => { + try { + const res = await fetch("/api/browse-directory"); + const result = await res.json(); + if (result.success && !result.cancelled && result.path) { + setWorkflowsDirectory(result.path); + setDefaultDir(result.path); + } + } catch { + setError("Failed to open directory picker"); + } + }, []); + + const handleChangeFolder = useCallback(async () => { + try { + const res = await fetch("/api/browse-directory"); + const result = await res.json(); + if (result.success && !result.cancelled && result.path) { + setWorkflowsDirectory(result.path); + setDefaultDir(result.path); + } + } catch { + setError("Failed to open directory picker"); + } + }, []); + + const handleBrowseOther = useCallback(async () => { + try { + const browseRes = await fetch("/api/browse-directory"); + const browseResult = await browseRes.json(); + + if ( + !browseResult.success || + browseResult.cancelled || + !browseResult.path + ) { + if (!browseResult.success && !browseResult.cancelled) { + setError(browseResult.error || "Failed to open directory picker"); + } + return; + } + + const dirPath = browseResult.path; + + setLoadingWorkflow(dirPath); + const loadRes = await fetch( + `/api/workflow?path=${encodeURIComponent(dirPath)}&load=true` + ); + const loadResult = await loadRes.json(); + setLoadingWorkflow(null); + + if (!loadResult.success) { + setError(loadResult.error || "No workflow file found in directory"); + return; + } + + onWorkflowLoaded(loadResult.workflow as WorkflowFile, dirPath); + onClose?.(); + } catch { + setLoadingWorkflow(null); + setError("Failed to open workflow"); + } + }, [onWorkflowLoaded, onClose]); + + const handleSelectWorkflow = useCallback( + async (entry: WorkflowListEntry) => { + setLoadingWorkflow(entry.directoryPath); + setError(null); + try { + const res = await fetch( + `/api/workflow?path=${encodeURIComponent(entry.directoryPath)}&load=true` + ); + const result = await res.json(); + + if (!result.success) { + setError(result.error || "Failed to load workflow"); + setLoadingWorkflow(null); + return; + } + + onWorkflowLoaded( + result.workflow as WorkflowFile, + entry.directoryPath + ); + onClose?.(); + } catch { + setError("Failed to load workflow"); + setLoadingWorkflow(null); + } + }, + [onWorkflowLoaded, onClose] + ); + + // State A: No default directory configured + if (defaultDir === null) { + return ( +
+ {onBack && ( +
+ +
+ )} +
+
+ + + +
+
+

+ Your Workflows +

+

+ Choose a folder that contains your workflow projects +

+
+ +
+
+ ); + } + + // State B: Default directory configured — show listing + return ( +
+ {/* Header */} +
+
+ {onBack && } +
+

+ Your Workflows +

+

+ {defaultDir} +

+
+
+
+ + {/* Workflow list */} +
+ {isLoading ? ( +
+
+
+ ) : error && workflows.length === 0 ? ( +
+

{error}

+
+ ) : ( +
+ {workflows.map((entry) => ( + + ))} +
+ )} + + {error && workflows.length > 0 && ( +

{error}

+ )} +
+ + {/* Footer */} +
+ + · + +
+
+ ); +}