import { useState, useCallback } from "react"; import { X, Loader2, Sparkles, CheckCircle, ChevronRight } from "lucide-react"; import type { ProjectInfo, ProjectCreateInput } from "../api"; import { registerProject } from "../api"; import { getAuthToken, setAuthToken, clearAuthToken } from "../auth"; import { DirectoryPicker } from "./DirectoryPicker"; import { suggestProjectName } from "../utils/projectDetection"; import { useNodes } from "../hooks/useNodes"; export interface SetupWizardModalProps { /** Called when a single project is registered */ onProjectRegistered: (project: ProjectInfo) => void; /** Called when wizard is closed (completed or cancelled) */ onClose?: () => void; } type WizardStep = "manual" | "complete"; interface WizardState { step: WizardStep; manualPath: string; manualName: string; manualIsolationMode: "in-process" | "child-process"; manualNodeId: string; isRegistering: boolean; error: string | null; } /** * Setup wizard for first-run project registration. * * Provides a polished onboarding experience with a directory picker * for selecting the project directory and auto-name suggestion. */ export function SetupWizardModal({ onProjectRegistered, onClose, }: SetupWizardModalProps) { const helpUrl = "https://github.com/runfusion/fusion/discussions"; const [isOpen, setIsOpen] = useState(true); const [state, setState] = useState({ step: "manual", manualPath: "", manualName: "", manualIsolationMode: "in-process", manualNodeId: "", isRegistering: false, error: null, }); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [authTokenInput, setAuthTokenInput] = useState(""); const [storedAuthToken, setStoredAuthToken] = useState(() => getAuthToken()); const { nodes, loading: nodesLoading } = useNodes(); const localNodeId = nodes.find((n) => n.type === "local")?.id; const handleClose = useCallback(() => { setIsOpen(false); onClose?.(); }, [onClose]); const handlePathChange = useCallback((path: string) => { setState((prev) => { const updates: Partial = { manualPath: path }; // Auto-suggest name when path changes and name is empty or was previously auto-suggested if (path && (!prev.manualName || prev.manualName === suggestProjectName(prev.manualPath))) { updates.manualName = suggestProjectName(path); } return { ...prev, ...updates }; }); }, []); const handleManualRegister = useCallback(async () => { if (!state.manualPath || !state.manualName) return; setState((prev) => ({ ...prev, isRegistering: true, error: null })); try { const input: ProjectCreateInput = { name: state.manualName, path: state.manualPath, isolationMode: state.manualIsolationMode, nodeId: state.manualNodeId || undefined, }; const result = await registerProject(input); onProjectRegistered(result); setState((prev) => ({ ...prev, step: "complete", isRegistering: false, })); } catch (err) { setState((prev) => ({ ...prev, isRegistering: false, error: err instanceof Error ? err.message : "Failed to register project", })); } }, [state.manualPath, state.manualName, state.manualIsolationMode, state.manualNodeId, onProjectRegistered]); const handleSetAuthToken = useCallback(() => { const token = authTokenInput.trim(); if (!token) return; setAuthToken(token); window.location.reload(); }, [authTokenInput]); const handleResetAuthToken = useCallback(() => { clearAuthToken(); setStoredAuthToken(undefined); setAuthTokenInput(""); window.location.reload(); }, []); if (!isOpen) return null; return (
{/* Header */}
Fusion

{state.step === "manual" && "Welcome to Fusion"} {state.step === "complete" && "Setup Complete!"}

{state.step !== "complete" && ( )}
{/* Content */}
{/* Manual Step */} {state.step === "manual" && (

Let's set up your first project. Browse to your project directory or type the path manually.

Select or type the absolute path to your project

setState((prev) => ({ ...prev, manualName: e.target.value })) } placeholder="my-project" />
{showAdvancedSettings && (
Runtime Node
setAuthTokenInput(e.target.value)} placeholder={storedAuthToken ? "Enter a new token to replace the stored one" : "Paste the auth token for this browser"} autoComplete="off" spellCheck={false} />
{storedAuthToken && ( )}

{storedAuthToken ? "A token is already stored in this browser. Updating or resetting it will reload the page." : "Store a token in this browser for authenticated dashboard requests, then reload the page."}

)}
{state.error && (
{state.error}
)}
)} {/* Complete Step */} {state.step === "complete" && (
{/* Footer */}
Need help? {state.step === "manual" && ( )} {state.step === "complete" && ( )}
); }