"use client"; import { useState, useEffect } from "react"; import { generateWorkflowId, useWorkflowStore } from "@/store/workflowStore"; import { ProviderType, ProviderSettings, NodeDefaultsConfig, LLMProvider, LLMModelType } from "@/types"; import { EnvStatusResponse } from "@/app/api/env-status/route"; import { loadNodeDefaults, saveNodeDefaults } from "@/store/utils/localStorage"; import { ProviderModel } from "@/lib/providers/types"; import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; // LLM provider and model options (mirrored from LLMGenerateNode) const LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [ { value: "google", label: "Google" }, { value: "openai", label: "OpenAI" }, ]; const LLM_MODELS: Record = { google: [ { value: "gemini-3-flash-preview", label: "Gemini 3 Flash" }, { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }, { value: "gemini-3-pro-preview", label: "Gemini 3.0 Pro" }, ], openai: [ { value: "gpt-4.1-mini", label: "GPT-4.1 Mini" }, { value: "gpt-4.1-nano", label: "GPT-4.1 Nano" }, ], }; // Provider icons const GeminiIcon = () => ( ); const ReplicateIcon = () => ( ); const FalIcon = () => ( ); // Get provider icon component const getProviderIcon = (provider: ProviderType) => { switch (provider) { case "gemini": return ; case "replicate": return ; case "fal": return ; default: return null; } }; interface ProjectSetupModalProps { isOpen: boolean; onClose: () => void; onSave: (id: string, name: string, directoryPath: string) => void; mode: "new" | "settings"; } export function ProjectSetupModal({ isOpen, onClose, onSave, mode, }: ProjectSetupModalProps) { const { workflowName, saveDirectoryPath, useExternalImageStorage, setUseExternalImageStorage, providerSettings, updateProviderApiKey, toggleProvider, } = useWorkflowStore(); // Tab state const [activeTab, setActiveTab] = useState<"project" | "providers" | "nodeDefaults">("project"); // Project tab state const [name, setName] = useState(""); const [directoryPath, setDirectoryPath] = useState(""); const [externalStorage, setExternalStorage] = useState(true); const [isValidating, setIsValidating] = useState(false); const [isBrowsing, setIsBrowsing] = useState(false); const [error, setError] = useState(null); // Provider tab state const [localProviders, setLocalProviders] = useState(providerSettings); const [showApiKey, setShowApiKey] = useState>({ gemini: false, openai: false, replicate: false, fal: false, kie: false, }); const [overrideActive, setOverrideActive] = useState>({ gemini: false, openai: false, replicate: false, fal: false, kie: false, }); const [envStatus, setEnvStatus] = useState(null); // Node defaults tab state const [localNodeDefaults, setLocalNodeDefaults] = useState({}); const [showImageModelDialog, setShowImageModelDialog] = useState(false); const [showVideoModelDialog, setShowVideoModelDialog] = useState(false); // Pre-fill when opening in settings mode useEffect(() => { if (isOpen) { // Reset to project tab when opening if (mode === "new") { setActiveTab("project"); } if (mode === "settings") { setName(workflowName || ""); setDirectoryPath(saveDirectoryPath || ""); setExternalStorage(useExternalImageStorage); } else if (mode === "new") { setName(""); setDirectoryPath(""); setExternalStorage(true); } // Sync local providers state setLocalProviders(providerSettings); setShowApiKey({ gemini: false, openai: false, replicate: false, fal: false, kie: false }); // Initialize override as active if user already has a key set setOverrideActive({ gemini: !!providerSettings.providers.gemini?.apiKey, openai: !!providerSettings.providers.openai?.apiKey, replicate: !!providerSettings.providers.replicate?.apiKey, fal: !!providerSettings.providers.fal?.apiKey, kie: !!providerSettings.providers.kie?.apiKey, }); setError(null); // Load node defaults setLocalNodeDefaults(loadNodeDefaults()); setShowImageModelDialog(false); setShowVideoModelDialog(false); // Fetch env status fetch("/api/env-status") .then((res) => res.json()) .then((data: EnvStatusResponse) => setEnvStatus(data)) .catch(() => setEnvStatus(null)); } }, [isOpen, mode, workflowName, saveDirectoryPath, useExternalImageStorage, providerSettings]); const handleBrowse = async () => { setIsBrowsing(true); setError(null); try { const response = await fetch("/api/browse-directory"); const result = await response.json(); if (!result.success) { setError(result.error || "Failed to open directory picker"); return; } if (result.cancelled) { return; } if (result.path) { setDirectoryPath(result.path); } } catch (err) { setError( `Failed to open directory picker: ${err instanceof Error ? err.message : "Unknown error"}` ); } finally { setIsBrowsing(false); } }; const handleSaveProject = async () => { if (!name.trim()) { setError("Project name is required"); return; } if (!directoryPath.trim()) { setError("Project directory is required"); return; } setIsValidating(true); setError(null); try { // Validate project directory exists const response = await fetch( `/api/workflow?path=${encodeURIComponent(directoryPath.trim())}` ); const result = await response.json(); if (!result.exists) { setError("Project directory does not exist"); setIsValidating(false); return; } if (!result.isDirectory) { setError("Project path is not a directory"); setIsValidating(false); return; } const id = mode === "new" ? generateWorkflowId() : useWorkflowStore.getState().workflowId || generateWorkflowId(); // Update external storage setting setUseExternalImageStorage(externalStorage); onSave(id, name.trim(), directoryPath.trim()); setIsValidating(false); } catch (err) { setError( `Failed to validate directory: ${err instanceof Error ? err.message : "Unknown error"}` ); setIsValidating(false); } }; const handleSaveProviders = () => { // Save each provider's settings const providerIds: ProviderType[] = ["gemini", "openai", "replicate", "fal"]; for (const providerId of providerIds) { const local = localProviders.providers[providerId]; const current = providerSettings.providers[providerId]; if (!local || !current) continue; // Update enabled state if changed if (local.enabled !== current.enabled) { toggleProvider(providerId, local.enabled); } // Update API key if changed if (local.apiKey !== current.apiKey) { updateProviderApiKey(providerId, local.apiKey); } } onClose(); }; const handleSaveNodeDefaults = () => { saveNodeDefaults(localNodeDefaults); onClose(); }; const handleSave = () => { if (activeTab === "project") { handleSaveProject(); } else if (activeTab === "providers") { handleSaveProviders(); } else { handleSaveNodeDefaults(); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !isValidating && !isBrowsing) { handleSave(); } if (e.key === "Escape") { onClose(); } }; const updateLocalProvider = ( providerId: ProviderType, updates: { enabled?: boolean; apiKey?: string | null } ) => { setLocalProviders((prev) => ({ providers: { ...prev.providers, [providerId]: { ...prev.providers[providerId], ...updates, }, }, })); }; if (!isOpen) return null; return (

{mode === "new" ? "New Project" : "Project Settings"}

{/* Tab Bar */}
{/* Project Tab Content */} {activeTab === "project" && (
setName(e.target.value)} placeholder="my-project" autoFocus className="w-full px-3 py-2 bg-neutral-900 border border-neutral-600 rounded text-neutral-100 text-sm focus:outline-none focus:border-neutral-500" />
setDirectoryPath(e.target.value)} placeholder="/Users/username/projects/my-project" className="flex-1 px-3 py-2 bg-neutral-900 border border-neutral-600 rounded text-neutral-100 text-sm focus:outline-none focus:border-neutral-500" />

Workflow files and images will be saved here. Subfolders for inputs and generations will be auto-created.

{error &&

{error}

}
)} {/* Providers Tab Content */} {activeTab === "providers" && (
{/* Gemini Provider */}
Google Gemini {envStatus?.gemini && !overrideActive.gemini ? (
Configured via .env
) : (
updateLocalProvider("gemini", { apiKey: e.target.value || null })} placeholder="AIza..." className="w-48 px-2 py-1 bg-neutral-800 border border-neutral-600 rounded text-neutral-100 text-xs focus:outline-none focus:border-neutral-500" /> {envStatus?.gemini && ( )}
)}
{/* OpenAI Provider */}
OpenAI {envStatus?.openai && !overrideActive.openai ? (
Configured via .env
) : (
updateLocalProvider("openai", { apiKey: e.target.value || null })} placeholder="sk-..." className="w-48 px-2 py-1 bg-neutral-800 border border-neutral-600 rounded text-neutral-100 text-xs focus:outline-none focus:border-neutral-500" /> {envStatus?.openai && ( )}
)}
{/* Replicate Provider */}
Replicate {envStatus?.replicate && !overrideActive.replicate ? (
Configured via .env
) : (
updateLocalProvider("replicate", { apiKey: e.target.value || null })} placeholder="r8_..." className="w-48 px-2 py-1 bg-neutral-800 border border-neutral-600 rounded text-neutral-100 text-xs focus:outline-none focus:border-neutral-500" /> {envStatus?.replicate && ( )}
)}
{/* fal.ai Provider */}
fal.ai {envStatus?.fal && !overrideActive.fal ? (
Configured via .env
) : (
updateLocalProvider("fal", { apiKey: e.target.value || null })} placeholder="..." className="w-48 px-2 py-1 bg-neutral-800 border border-neutral-600 rounded text-neutral-100 text-xs focus:outline-none focus:border-neutral-500" /> {envStatus?.fal && ( )}
)}

Add API keys via .env.local for better security. Keys added here override .env and are stored in your browser.

)} {/* Node Defaults Tab Content */} {activeTab === "nodeDefaults" && (
{/* GenerateImage Section */}
Default Image Model
{localNodeDefaults.generateImage?.selectedModel ? ( <>
{getProviderIcon(localNodeDefaults.generateImage.selectedModel.provider)} {localNodeDefaults.generateImage.selectedModel.displayName}
) : ( <> System default (Gemini nano-banana-pro) )}
{/* GenerateVideo Section */}
Default Video Model
{localNodeDefaults.generateVideo?.selectedModel ? ( <>
{getProviderIcon(localNodeDefaults.generateVideo.selectedModel.provider)} {localNodeDefaults.generateVideo.selectedModel.displayName}
) : ( <> None set (select on first use) )}
{/* LLM Section */}
Default LLM Settings {localNodeDefaults.llm && ( )}
{!localNodeDefaults.llm ? (

Using system defaults (Google Gemini 3 Flash)

) : null} {/* Provider dropdown */}
{/* Model dropdown */}
{/* Temperature slider */}
{ setLocalNodeDefaults(prev => ({ ...prev, llm: { ...prev.llm, temperature: parseFloat(e.target.value) } })); }} className="flex-1 h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-neutral-400" />
{/* Max Tokens slider */}
{ setLocalNodeDefaults(prev => ({ ...prev, llm: { ...prev.llm, maxTokens: parseInt(e.target.value, 10) } })); }} className="flex-1 h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-neutral-400" />

These defaults are applied when creating nodes via keyboard shortcuts (Shift+G, Shift+L, etc).

)}
{/* Model Selection Dialogs */} {showImageModelDialog && ( setShowImageModelDialog(false)} onModelSelected={(model: ProviderModel) => { setLocalNodeDefaults(prev => ({ ...prev, generateImage: { ...prev.generateImage, selectedModel: { provider: model.provider, modelId: model.id, displayName: model.name, } } })); setShowImageModelDialog(false); }} initialCapabilityFilter="image" /> )} {showVideoModelDialog && ( setShowVideoModelDialog(false)} onModelSelected={(model: ProviderModel) => { setLocalNodeDefaults(prev => ({ ...prev, generateVideo: { ...prev.generateVideo, selectedModel: { provider: model.provider, modelId: model.id, displayName: model.name, } } })); setShowVideoModelDialog(false); }} initialCapabilityFilter="video" /> )}
); }