@ -496,89 +496,10 @@ function GenerateImageControls({ node }: { node: Node }) {
function GenerateVideoControls ( { node } : { node : Node } ) {
function GenerateVideoControls ( { node } : { node : Node } ) {
const nodeData = node . data as GenerateVideoNodeData ;
const nodeData = node . data as GenerateVideoNodeData ;
const updateNodeData = useWorkflowStore ( ( state ) = > state . updateNodeData ) ;
const updateNodeData = useWorkflowStore ( ( state ) = > state . updateNodeData ) ;
const { replicateApiKey , falApiKey , kieApiKey , replicateEnabled , kieEnabled } = useProviderApiKeys ( ) ;
const [ externalModels , setExternalModels ] = useState < ProviderModel [ ] > ( [ ] ) ;
const [ isLoadingModels , setIsLoadingModels ] = useState ( false ) ;
const [ modelsFetchError , setModelsFetchError ] = useState < string | null > ( null ) ;
const [ isBrowseDialogOpen , setIsBrowseDialogOpen ] = useState ( false ) ;
const [ isBrowseDialogOpen , setIsBrowseDialogOpen ] = useState ( false ) ;
const currentProvider : ProviderType = nodeData . selectedModel ? . provider || "fal" ;
const currentProvider : ProviderType = nodeData . selectedModel ? . provider || "fal" ;
const enabledProviders = useMemo ( ( ) = > {
const providers : { id : ProviderType ; name : string } [ ] = [ ] ;
providers . push ( { id : "fal" , name : "fal.ai" } ) ;
if ( replicateEnabled && replicateApiKey ) {
providers . push ( { id : "replicate" , name : "Replicate" } ) ;
}
if ( kieEnabled && kieApiKey ) {
providers . push ( { id : "kie" , name : "Kie.ai" } ) ;
}
return providers ;
} , [ replicateEnabled , replicateApiKey , kieEnabled , kieApiKey ] ) ;
const fetchModels = useCallback ( async ( ) = > {
setIsLoadingModels ( true ) ;
setModelsFetchError ( null ) ;
try {
const capabilities = VIDEO_CAPABILITIES . join ( "," ) ;
const headers : HeadersInit = { } ;
if ( replicateApiKey ) headers [ "X-Replicate-Key" ] = replicateApiKey ;
if ( falApiKey ) headers [ "X-Fal-Key" ] = falApiKey ;
if ( kieApiKey ) headers [ "X-Kie-Key" ] = kieApiKey ;
const response = await deduplicatedFetch ( ` /api/models?provider= ${ currentProvider } &capabilities= ${ capabilities } ` , { headers } ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
setExternalModels ( data . models || [ ] ) ;
setModelsFetchError ( null ) ;
} else {
const errorData = await response . json ( ) . catch ( ( ) = > ( { } ) ) ;
setExternalModels ( [ ] ) ;
setModelsFetchError ( errorData . error || ` Failed to load models ` ) ;
}
} catch ( error ) {
console . error ( "Failed to fetch models:" , error ) ;
setExternalModels ( [ ] ) ;
setModelsFetchError ( "Failed to load models" ) ;
} finally {
setIsLoadingModels ( false ) ;
}
} , [ currentProvider , replicateApiKey , falApiKey , kieApiKey ] ) ;
useEffect ( ( ) = > {
fetchModels ( ) ;
} , [ fetchModels ] ) ;
const handleProviderChange = useCallback (
( e : React.ChangeEvent < HTMLSelectElement > ) = > {
const provider = e . target . value as ProviderType ;
const newSelectedModel : SelectedModel = {
provider ,
modelId : "" ,
displayName : "Select model..." ,
} ;
updateNodeData ( node . id , { selectedModel : newSelectedModel , parameters : { } } ) ;
} ,
[ node . id , updateNodeData ]
) ;
const handleModelChange = useCallback (
( e : React.ChangeEvent < HTMLSelectElement > ) = > {
const modelId = e . target . value ;
const model = externalModels . find ( m = > m . id === modelId ) ;
if ( model ) {
const newSelectedModel : SelectedModel = {
provider : currentProvider ,
modelId : model.id ,
displayName : model.name ,
capabilities : model.capabilities ,
} ;
updateNodeData ( node . id , { selectedModel : newSelectedModel , parameters : { } } ) ;
}
} ,
[ node . id , currentProvider , externalModels , updateNodeData ]
) ;
const handleParametersChange = useCallback (
const handleParametersChange = useCallback (
( parameters : Record < string , unknown > ) = > {
( parameters : Record < string , unknown > ) = > {
updateNodeData ( node . id , { parameters } ) ;
updateNodeData ( node . id , { parameters } ) ;
@ -600,43 +521,44 @@ function GenerateVideoControls({ node }: { node: Node }) {
return (
return (
< >
< >
< div className = "space-y-3" >
< div className = "space-y-3" >
< div >
{ /* Model name + provider with link */ }
< label className = "block text-xs font-medium text-neutral-300 mb-1" > Provider < / label >
< div className = "border-t border-neutral-700 pt-3" >
< select
< div className = "flex items-start gap-2" >
value = { currentProvider }
< div className = "flex-1 min-w-0" >
onChange = { handleProviderChange }
< div className = "text-sm text-neutral-100 truncate" >
className = "nodrag nopan w-full px-2 py-1 text-xs bg-neutral-700 border border-neutral-600 rounded text-neutral-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
{ nodeData . selectedModel ? . displayName || "Select model..." }
>
< / div >
{ enabledProviders . map ( p = > (
< div className = "flex items-center gap-1 mt-0.5" >
< option key = { p . id } value = { p . id } > { p . name } < / option >
< span className = "text-[10px] text-neutral-500 truncate" >
) ) }
{ getProviderDisplayName ( currentProvider ) }
< / select >
< / span >
< / div >
{ nodeData . selectedModel ? . modelId && (
< a
< div >
href = { getModelPageUrl ( currentProvider , nodeData . selectedModel . modelId ) || "#" }
< label className = "block text-xs font-medium text-neutral-300 mb-1" > Model < / label >
target = "_blank"
< div className = "flex gap-1" >
rel = "noopener noreferrer"
< select
className = "text-neutral-500 hover:text-neutral-300 transition-colors"
value = { nodeData . selectedModel ? . modelId || "" }
title = { ` View on ${ getProviderDisplayName ( currentProvider ) } ` }
onChange = { handleModelChange }
onClick = { ( e ) = > {
className = "nodrag nopan flex-1 px-2 py-1 text-xs bg-neutral-700 border border-neutral-600 rounded text-neutral-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
if ( ! getModelPageUrl ( currentProvider , nodeData . selectedModel ? . modelId || "" ) ) {
disabled = { isLoadingModels }
e . preventDefault ( ) ;
>
}
< option value = "" > Select model . . . < / option >
} }
{ externalModels . map ( m = > (
>
< option key = { m . id } value = { m . id } > { m . name } < / option >
< svg className = "w-3 h-3" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
) ) }
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" / >
< / select >
< / svg >
< / a >
) }
< / div >
< / div >
< button
< button
onClick = { ( ) = > setIsBrowseDialogOpen ( true ) }
onClick = { ( ) = > setIsBrowseDialogOpen ( true ) }
className = "nodrag nopan px-2 py-1 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
className = "nodrag nopan shrink-0 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
>
>
Browse
Browse
< / button >
< / button >
< / div >
< / div >
{ modelsFetchError && (
< p className = "text-xs text-red-400 mt-1" > { modelsFetchError } < / p >
) }
< / div >
< / div >
{ nodeData . selectedModel ? . modelId && (
{ nodeData . selectedModel ? . modelId && (