From 0364a8a3ea95d0b8d571adbf6c74cd4ee8c42753 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 9 Jan 2026 15:32:17 +1300 Subject: [PATCH] feat(01-01): add Providers tab to ProjectSetupModal Add tabbed interface with Project and Providers tabs. Providers tab shows Gemini (env var configured), Replicate, and fal.ai with toggle switches and API key inputs. Settings persist to localStorage on save. Co-Authored-By: Claude Opus 4.5 --- src/components/ProjectSetupModal.tsx | 331 +++++++++++++++++++++------ 1 file changed, 267 insertions(+), 64 deletions(-) diff --git a/src/components/ProjectSetupModal.tsx b/src/components/ProjectSetupModal.tsx index b966ee70..375e0cc1 100644 --- a/src/components/ProjectSetupModal.tsx +++ b/src/components/ProjectSetupModal.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { generateWorkflowId, useWorkflowStore } from "@/store/workflowStore"; +import { ProviderType, ProviderSettings } from "@/types"; interface ProjectSetupModalProps { isOpen: boolean; @@ -16,8 +17,20 @@ export function ProjectSetupModal({ onSave, mode, }: ProjectSetupModalProps) { - const { workflowName, saveDirectoryPath, useExternalImageStorage, setUseExternalImageStorage } = useWorkflowStore(); + const { + workflowName, + saveDirectoryPath, + useExternalImageStorage, + setUseExternalImageStorage, + providerSettings, + updateProviderApiKey, + toggleProvider, + } = useWorkflowStore(); + // Tab state + const [activeTab, setActiveTab] = useState<"project" | "providers">("project"); + + // Project tab state const [name, setName] = useState(""); const [directoryPath, setDirectoryPath] = useState(""); const [externalStorage, setExternalStorage] = useState(true); @@ -25,18 +38,38 @@ export function ProjectSetupModal({ const [isBrowsing, setIsBrowsing] = useState(false); const [error, setError] = useState(null); + // Provider tab state + const [localProviders, setLocalProviders] = useState(providerSettings); + const [showApiKey, setShowApiKey] = useState>({ + gemini: false, + replicate: false, + fal: false, + }); + // Pre-fill when opening in settings mode useEffect(() => { - if (isOpen && mode === "settings") { - setName(workflowName || ""); - setDirectoryPath(saveDirectoryPath || ""); - setExternalStorage(useExternalImageStorage); - } else if (isOpen && mode === "new") { - setName(""); - setDirectoryPath(""); - setExternalStorage(true); + 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, replicate: false, fal: false }); + setError(null); } - }, [isOpen, mode, workflowName, saveDirectoryPath, useExternalImageStorage]); + }, [isOpen, mode, workflowName, saveDirectoryPath, useExternalImageStorage, providerSettings]); const handleBrowse = async () => { setIsBrowsing(true); @@ -67,7 +100,7 @@ export function ProjectSetupModal({ } }; - const handleSave = async () => { + const handleSaveProject = async () => { if (!name.trim()) { setError("Project name is required"); return; @@ -113,6 +146,34 @@ export function ProjectSetupModal({ } }; + const handleSaveProviders = () => { + // Save each provider's settings + const providerIds: ProviderType[] = ["gemini", "replicate", "fal"]; + for (const providerId of providerIds) { + const local = localProviders.providers[providerId]; + const current = providerSettings.providers[providerId]; + + // 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 handleSave = () => { + if (activeTab === "project") { + handleSaveProject(); + } else { + handleSaveProviders(); + } + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !isValidating && !isBrowsing) { handleSave(); @@ -122,6 +183,21 @@ export function ProjectSetupModal({ } }; + 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 ( @@ -134,66 +210,190 @@ export function ProjectSetupModal({ {mode === "new" ? "New Project" : "Project Settings"} -
-
- - 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" - /> -
+ {/* Tab Bar */} +
+ + +
-
- -
+ {/* Project Tab Content */} + {activeTab === "project" && ( +
+
+ 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" + value={name} + onChange={(e) => 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" /> -
-

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

-
-
-
+ {/* Providers Tab Content */} + {activeTab === "providers" && ( +
+ {/* Gemini Provider */} +
+
+
+ + {localProviders.providers.gemini.name} + + + Default + +
+ Enabled +
+

+ Configured via GEMINI_API_KEY environment variable +

+
+ + {/* Replicate Provider */} +
+
+ + {localProviders.providers.replicate.name} + + +
+ {localProviders.providers.replicate.enabled && ( +
+ +
+ updateLocalProvider("replicate", { apiKey: e.target.value || null })} + placeholder="r8_..." + className="flex-1 px-3 py-2 bg-neutral-800 border border-neutral-600 rounded text-neutral-100 text-sm focus:outline-none focus:border-neutral-500" + /> + +
+
+ )} +
+ + {/* fal.ai Provider */} +
+
+ + {localProviders.providers.fal.name} + + +
+ {localProviders.providers.fal.enabled && ( +
+ +
+ updateLocalProvider("fal", { apiKey: e.target.value || null })} + placeholder="fal_..." + className="flex-1 px-3 py-2 bg-neutral-800 border border-neutral-600 rounded text-neutral-100 text-sm focus:outline-none focus:border-neutral-500" + /> + +
+
+ )} +
+ +

+ API keys are stored locally in your browser and never sent to our servers. +

+
+ )}