@ -2,8 +2,64 @@
import { useState , useEffect } from "react" ;
import { useState , useEffect } from "react" ;
import { generateWorkflowId , useWorkflowStore } from "@/store/workflowStore" ;
import { generateWorkflowId , useWorkflowStore } from "@/store/workflowStore" ;
import { ProviderType , ProviderSettings } from "@/types" ;
import { ProviderType , ProviderSettings , NodeDefaultsConfig , LLMProvider , LLMModelType } from "@/types" ;
import { EnvStatusResponse } from "@/app/api/env-status/route" ;
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 < LLMProvider , { value : LLMModelType ; label : string } [ ] > = {
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 = ( ) = > (
< svg className = "w-4 h-4" viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2L14.5 9.5L22 12L14.5 14.5L12 22L9.5 14.5L2 12L9.5 9.5L12 2Z" / >
< / svg >
) ;
const ReplicateIcon = ( ) = > (
< svg className = "w-4 h-4" viewBox = "0 0 1000 1000" fill = "currentColor" >
< polygon points = "1000,427.6 1000,540.6 603.4,540.6 603.4,1000 477,1000 477,427.6" / >
< polygon points = "1000,213.8 1000,327 364.8,327 364.8,1000 238.4,1000 238.4,213.8" / >
< polygon points = "1000,0 1000,113.2 126.4,113.2 126.4,1000 0,1000 0,0" / >
< / svg >
) ;
const FalIcon = ( ) = > (
< svg className = "w-4 h-4" viewBox = "0 0 1855 1855" fill = "currentColor" >
< path fillRule = "evenodd" clipRule = "evenodd" d = "M1181.65 78C1212.05 78 1236.42 101.947 1239.32 131.261C1265.25 392.744 1480.07 600.836 1750.02 625.948C1780.28 628.764 1805 652.366 1805 681.816V1174.18C1805 1203.63 1780.28 1227.24 1750.02 1230.05C1480.07 1255.16 1265.25 1463.26 1239.32 1724.74C1236.42 1754.05 1212.05 1778 1181.65 1778H673.354C642.951 1778 618.585 1754.05 615.678 1724.74C589.754 1463.26 374.927 1255.16 104.984 1230.05C74.7212 1227.24 50 1203.63 50 1174.18V681.816C50 652.366 74.7213 628.764 104.984 625.948C374.927 600.836 589.754 392.744 615.678 131.261C618.585 101.946 642.951 78 673.353 78H1181.65ZM402.377 926.561C402.377 1209.41 638.826 1438.71 930.501 1438.71C1222.18 1438.71 1458.63 1209.41 1458.63 926.561C1458.63 643.709 1222.18 414.412 930.501 414.412C638.826 414.412 402.377 643.709 402.377 926.561Z" / >
< / svg >
) ;
// Get provider icon component
const getProviderIcon = ( provider : ProviderType ) = > {
switch ( provider ) {
case "gemini" :
return < GeminiIcon / > ;
case "replicate" :
return < ReplicateIcon / > ;
case "fal" :
return < FalIcon / > ;
default :
return null ;
}
} ;
interface ProjectSetupModalProps {
interface ProjectSetupModalProps {
isOpen : boolean ;
isOpen : boolean ;
@ -29,7 +85,7 @@ export function ProjectSetupModal({
} = useWorkflowStore ( ) ;
} = useWorkflowStore ( ) ;
// Tab state
// Tab state
const [ activeTab , setActiveTab ] = useState < "project" | "providers" > ( "project" ) ;
const [ activeTab , setActiveTab ] = useState < "project" | "providers" | "nodeDefaults" > ( "project" ) ;
// Project tab state
// Project tab state
const [ name , setName ] = useState ( "" ) ;
const [ name , setName ] = useState ( "" ) ;
@ -55,6 +111,11 @@ export function ProjectSetupModal({
} ) ;
} ) ;
const [ envStatus , setEnvStatus ] = useState < EnvStatusResponse | null > ( null ) ;
const [ envStatus , setEnvStatus ] = useState < EnvStatusResponse | null > ( null ) ;
// Node defaults tab state
const [ localNodeDefaults , setLocalNodeDefaults ] = useState < NodeDefaultsConfig > ( { } ) ;
const [ showImageModelDialog , setShowImageModelDialog ] = useState ( false ) ;
const [ showVideoModelDialog , setShowVideoModelDialog ] = useState ( false ) ;
// Pre-fill when opening in settings mode
// Pre-fill when opening in settings mode
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( isOpen ) {
if ( isOpen ) {
@ -85,6 +146,11 @@ export function ProjectSetupModal({
} ) ;
} ) ;
setError ( null ) ;
setError ( null ) ;
// Load node defaults
setLocalNodeDefaults ( loadNodeDefaults ( ) ) ;
setShowImageModelDialog ( false ) ;
setShowVideoModelDialog ( false ) ;
// Fetch env status
// Fetch env status
fetch ( "/api/env-status" )
fetch ( "/api/env-status" )
. then ( ( res ) = > res . json ( ) )
. then ( ( res ) = > res . json ( ) )
@ -190,11 +256,18 @@ export function ProjectSetupModal({
onClose ( ) ;
onClose ( ) ;
} ;
} ;
const handleSaveNodeDefaults = ( ) = > {
saveNodeDefaults ( localNodeDefaults ) ;
onClose ( ) ;
} ;
const handleSave = ( ) = > {
const handleSave = ( ) = > {
if ( activeTab === "project" ) {
if ( activeTab === "project" ) {
handleSaveProject ( ) ;
handleSaveProject ( ) ;
} else {
} else if ( activeTab === "providers" ) {
handleSaveProviders ( ) ;
handleSaveProviders ( ) ;
} else {
handleSaveNodeDefaults ( ) ;
}
}
} ;
} ;
@ -248,6 +321,12 @@ export function ProjectSetupModal({
>
>
Providers
Providers
< / button >
< / button >
< button
onClick = { ( ) = > setActiveTab ( "nodeDefaults" ) }
className = { ` pb-2 text-sm ${ activeTab === "nodeDefaults" ? "text-neutral-100 border-b-2 border-white" : "text-neutral-400" } ` }
>
Node Defaults
< / button >
< / div >
< / div >
{ /* Project Tab Content */ }
{ /* Project Tab Content */ }
@ -515,6 +594,199 @@ export function ProjectSetupModal({
< / div >
< / div >
) }
) }
{ /* Node Defaults Tab Content */ }
{ activeTab === "nodeDefaults" && (
< div className = "space-y-3" >
{ /* GenerateImage Section */ }
< div className = "p-3 bg-neutral-900 rounded-lg border border-neutral-700" >
< div className = "flex items-center justify-between" >
< span className = "text-sm font-medium text-neutral-100" > Default Image Model < / span >
< div className = "flex items-center gap-2" >
{ localNodeDefaults . generateImage ? . selectedModel ? (
< >
< div className = "flex items-center gap-1.5 text-xs text-neutral-300" >
{ getProviderIcon ( localNodeDefaults . generateImage . selectedModel . provider ) }
< span className = "truncate max-w-[150px]" >
{ localNodeDefaults . generateImage . selectedModel . displayName }
< / span >
< / div >
< button
type = "button"
onClick = { ( ) = > setShowImageModelDialog ( true ) }
className = "px-2 py-1 text-xs bg-neutral-700 hover:bg-neutral-600 text-neutral-200 rounded transition-colors"
>
Change
< / button >
< button
type = "button"
onClick = { ( ) = > {
const { generateImage , . . . rest } = localNodeDefaults ;
setLocalNodeDefaults ( rest ) ;
} }
className = "text-xs text-neutral-400 hover:text-neutral-200"
>
Clear
< / button >
< / >
) : (
< >
< span className = "text-xs text-neutral-500" > System default ( Gemini nano - banana - pro ) < / span >
< button
type = "button"
onClick = { ( ) = > setShowImageModelDialog ( true ) }
className = "px-2 py-1 text-xs bg-neutral-700 hover:bg-neutral-600 text-neutral-200 rounded transition-colors"
>
Select Model
< / button >
< / >
) }
< / div >
< / div >
< / div >
{ /* GenerateVideo Section */ }
< div className = "p-3 bg-neutral-900 rounded-lg border border-neutral-700" >
< div className = "flex items-center justify-between" >
< span className = "text-sm font-medium text-neutral-100" > Default Video Model < / span >
< div className = "flex items-center gap-2" >
{ localNodeDefaults . generateVideo ? . selectedModel ? (
< >
< div className = "flex items-center gap-1.5 text-xs text-neutral-300" >
{ getProviderIcon ( localNodeDefaults . generateVideo . selectedModel . provider ) }
< span className = "truncate max-w-[150px]" >
{ localNodeDefaults . generateVideo . selectedModel . displayName }
< / span >
< / div >
< button
type = "button"
onClick = { ( ) = > setShowVideoModelDialog ( true ) }
className = "px-2 py-1 text-xs bg-neutral-700 hover:bg-neutral-600 text-neutral-200 rounded transition-colors"
>
Change
< / button >
< button
type = "button"
onClick = { ( ) = > {
const { generateVideo , . . . rest } = localNodeDefaults ;
setLocalNodeDefaults ( rest ) ;
} }
className = "text-xs text-neutral-400 hover:text-neutral-200"
>
Clear
< / button >
< / >
) : (
< >
< span className = "text-xs text-neutral-500" > None set ( select on first use ) < / span >
< button
type = "button"
onClick = { ( ) = > setShowVideoModelDialog ( true ) }
className = "px-2 py-1 text-xs bg-neutral-700 hover:bg-neutral-600 text-neutral-200 rounded transition-colors"
>
Select Model
< / button >
< / >
) }
< / div >
< / div >
< / div >
{ /* LLM Section */ }
< div className = "p-3 bg-neutral-900 rounded-lg border border-neutral-700" >
< div className = "flex flex-col gap-3" >
< div className = "flex items-center justify-between" >
< span className = "text-sm font-medium text-neutral-100" > Default LLM Settings < / span >
{ localNodeDefaults . llm && (
< button
type = "button"
onClick = { ( ) = > {
const { llm , . . . rest } = localNodeDefaults ;
setLocalNodeDefaults ( rest ) ;
} }
className = "text-xs text-neutral-400 hover:text-neutral-200"
>
Clear
< / button >
) }
< / div >
{ ! localNodeDefaults . llm ? (
< p className = "text-xs text-neutral-500" > Using system defaults ( Google Gemini 3 Flash ) < / p >
) : null }
{ /* Provider dropdown */ }
< div className = "flex items-center gap-2" >
< label className = "text-xs text-neutral-400 w-20" > Provider < / label >
< select
value = { localNodeDefaults . llm ? . provider || "google" }
onChange = { ( e ) = > {
const newProvider = e . target . value as LLMProvider ;
const firstModelForProvider = LLM_MODELS [ newProvider ] [ 0 ] . value ;
setLocalNodeDefaults ( prev = > ( {
. . . prev ,
llm : {
. . . prev . llm ,
provider : newProvider ,
model : firstModelForProvider ,
}
} ) ) ;
} }
className = "flex-1 px-2 py-1 text-xs bg-neutral-800 border border-neutral-600 rounded text-neutral-100 focus:outline-none focus:border-neutral-500"
>
{ LLM_PROVIDERS . map ( ( p ) = > (
< option key = { p . value } value = { p . value } > { p . label } < / option >
) ) }
< / select >
< / div >
{ /* Model dropdown */ }
< div className = "flex items-center gap-2" >
< label className = "text-xs text-neutral-400 w-20" > Model < / label >
< select
value = { localNodeDefaults . llm ? . model || LLM_MODELS [ localNodeDefaults . llm ? . provider || "google" ] [ 0 ] . value }
onChange = { ( e ) = > {
setLocalNodeDefaults ( prev = > ( {
. . . prev ,
llm : { . . . prev . llm , model : e.target.value as LLMModelType }
} ) ) ;
} }
className = "flex-1 px-2 py-1 text-xs bg-neutral-800 border border-neutral-600 rounded text-neutral-100 focus:outline-none focus:border-neutral-500"
>
{ LLM_MODELS [ localNodeDefaults . llm ? . provider || "google" ] . map ( ( m ) = > (
< option key = { m . value } value = { m . value } > { m . label } < / option >
) ) }
< / select >
< / div >
{ /* Temperature slider */ }
< div className = "flex items-center gap-2" >
< label className = "text-xs text-neutral-400 w-20" >
Temp : { ( localNodeDefaults . llm ? . temperature ? ? 0.7 ) . toFixed ( 1 ) }
< / label >
< input
type = "range"
min = "0"
max = "2"
step = "0.1"
value = { localNodeDefaults . llm ? . temperature ? ? 0.7 }
onChange = { ( e ) = > {
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"
/ >
< / div >
< / div >
< / div >
< p className = "text-xs text-neutral-500 mt-2" >
These defaults are applied when creating nodes via keyboard shortcuts ( Shift + G , Shift + L , etc ) .
< / p >
< / div >
) }
< div className = "flex justify-end gap-2 mt-6" >
< div className = "flex justify-end gap-2 mt-6" >
< button
< button
onClick = { onClose }
onClick = { onClose }