|
|
|
@ -14,6 +14,7 @@ import { |
|
|
|
WorkflowEdge, |
|
|
|
NodeType, |
|
|
|
NanoBananaNodeData, |
|
|
|
GenerateVideoNodeData, |
|
|
|
OutputGalleryNodeData, |
|
|
|
WorkflowNodeData, |
|
|
|
ImageHistoryItem, |
|
|
|
@ -21,6 +22,8 @@ import { |
|
|
|
GroupColor, |
|
|
|
ProviderType, |
|
|
|
ProviderSettings, |
|
|
|
ProviderEnvStatus, |
|
|
|
SelectedModel, |
|
|
|
RecentModel, |
|
|
|
CanvasNavigationSettings, |
|
|
|
MatchMode, |
|
|
|
@ -336,9 +339,11 @@ interface WorkflowStore { |
|
|
|
|
|
|
|
// Provider settings state
|
|
|
|
providerSettings: ProviderSettings; |
|
|
|
providerEnvStatus: ProviderEnvStatus | null; |
|
|
|
|
|
|
|
// Provider settings actions
|
|
|
|
updateProviderSettings: (settings: ProviderSettings) => void; |
|
|
|
updateProviderEnvStatus: (status: ProviderEnvStatus) => void; |
|
|
|
updateProviderApiKey: (providerId: ProviderType, apiKey: string | null) => void; |
|
|
|
toggleProvider: (providerId: ProviderType, enabled: boolean) => void; |
|
|
|
|
|
|
|
@ -528,6 +533,65 @@ function syncUndoFlags(set: (partial: Partial<WorkflowStore>) => void): void { |
|
|
|
set({ canUndo: undoManager.canUndo, canRedo: undoManager.canRedo }); |
|
|
|
} |
|
|
|
|
|
|
|
function hasProviderCredential( |
|
|
|
providerSettings: ProviderSettings, |
|
|
|
providerEnvStatus: ProviderEnvStatus | null, |
|
|
|
provider: ProviderType |
|
|
|
): boolean { |
|
|
|
return Boolean( |
|
|
|
providerSettings.providers[provider]?.apiKey || |
|
|
|
providerEnvStatus?.[provider] |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
function shouldPromoteFallbackModel( |
|
|
|
data: { selectedModel?: SelectedModel; fallbackModel?: SelectedModel }, |
|
|
|
providerSettings: ProviderSettings, |
|
|
|
providerEnvStatus: ProviderEnvStatus | null |
|
|
|
): boolean { |
|
|
|
const fallbackProvider = data.fallbackModel?.provider; |
|
|
|
return Boolean( |
|
|
|
data.selectedModel?.provider === "gemini" && |
|
|
|
fallbackProvider && |
|
|
|
fallbackProvider !== "gemini" && |
|
|
|
!hasProviderCredential(providerSettings, providerEnvStatus, "gemini") && |
|
|
|
hasProviderCredential(providerSettings, providerEnvStatus, fallbackProvider) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
function promoteUnavailableGeminiFallbacks( |
|
|
|
nodes: WorkflowNode[], |
|
|
|
providerSettings: ProviderSettings, |
|
|
|
providerEnvStatus: ProviderEnvStatus | null |
|
|
|
): { nodes: WorkflowNode[]; changed: boolean } { |
|
|
|
let changed = false; |
|
|
|
const nextNodes = nodes.map((node) => { |
|
|
|
if (node.type !== "nanoBanana" && node.type !== "generateVideo") return node; |
|
|
|
const data = node.data as NanoBananaNodeData | GenerateVideoNodeData; |
|
|
|
if (!shouldPromoteFallbackModel(data, providerSettings, providerEnvStatus)) return node; |
|
|
|
|
|
|
|
changed = true; |
|
|
|
return { |
|
|
|
...node, |
|
|
|
data: { |
|
|
|
...data, |
|
|
|
selectedModel: data.fallbackModel, |
|
|
|
parameters: data.fallbackParameters ?? data.parameters ?? {}, |
|
|
|
fallbackModel: undefined, |
|
|
|
fallbackParameters: undefined, |
|
|
|
status: data.status === "error" ? "idle" : data.status, |
|
|
|
error: data.status === "error" ? null : data.error, |
|
|
|
__usedFallback: undefined, |
|
|
|
__fallbackModelUsed: undefined, |
|
|
|
__primaryError: undefined, |
|
|
|
inputSchema: undefined, |
|
|
|
} as WorkflowNodeData, |
|
|
|
} as WorkflowNode; |
|
|
|
}) as WorkflowNode[]; |
|
|
|
|
|
|
|
return { nodes: nextNodes, changed }; |
|
|
|
} |
|
|
|
|
|
|
|
const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({ |
|
|
|
nodes: [], |
|
|
|
edges: [], |
|
|
|
@ -562,6 +626,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({ |
|
|
|
|
|
|
|
// Provider settings initial state
|
|
|
|
providerSettings: getProviderSettings(), |
|
|
|
providerEnvStatus: null, |
|
|
|
|
|
|
|
// Model search dialog initial state
|
|
|
|
modelSearchOpen: false, |
|
|
|
@ -1216,6 +1281,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({ |
|
|
|
getNodes: () => get().nodes, |
|
|
|
signal, |
|
|
|
providerSettings: get().providerSettings, |
|
|
|
providerEnvStatus: get().providerEnvStatus, |
|
|
|
addIncurredCost: (cost: number) => get().addIncurredCost(cost), |
|
|
|
addToGlobalHistory: (item) => get().addToGlobalHistory(item), |
|
|
|
generationsPath: get().generationsPath, |
|
|
|
@ -2163,6 +2229,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({ |
|
|
|
|
|
|
|
// Migrate legacy nanoBanana nodes: derive selectedModel from model field if missing
|
|
|
|
const providerSettings = get().providerSettings; |
|
|
|
const providerEnvStatus = get().providerEnvStatus; |
|
|
|
workflow.nodes = workflow.nodes.map((node) => { |
|
|
|
if (node.type === "nanoBanana") { |
|
|
|
const data = node.data as NanoBananaNodeData; |
|
|
|
@ -2171,7 +2238,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({ |
|
|
|
...node, |
|
|
|
data: { |
|
|
|
...data, |
|
|
|
selectedModel: createDefaultImageModel(data.model, providerSettings), |
|
|
|
selectedModel: createDefaultImageModel(data.model, providerSettings, providerEnvStatus), |
|
|
|
}, |
|
|
|
}; |
|
|
|
} |
|
|
|
@ -2232,6 +2299,18 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const promotedModels = promoteUnavailableGeminiFallbacks( |
|
|
|
hydratedWorkflow.nodes as WorkflowNode[], |
|
|
|
providerSettings, |
|
|
|
providerEnvStatus |
|
|
|
); |
|
|
|
if (promotedModels.changed) { |
|
|
|
hydratedWorkflow = { |
|
|
|
...hydratedWorkflow, |
|
|
|
nodes: promotedModels.nodes, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
// Load cost data for this workflow
|
|
|
|
const costData = workflow.id ? loadWorkflowCostData(workflow.id) : null; |
|
|
|
|
|
|
|
@ -2260,7 +2339,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({ |
|
|
|
? joinBrowserFileSystemPath(directoryPath, "generations") |
|
|
|
: null), |
|
|
|
lastSavedAt: savedConfig?.lastSavedAt || null, |
|
|
|
hasUnsavedChanges: false, |
|
|
|
hasUnsavedChanges: promotedModels.changed, |
|
|
|
// Restore cost data
|
|
|
|
incurredCost: costData?.incurredCost || 0, |
|
|
|
// Track where imageRefs are valid from
|
|
|
|
@ -2655,10 +2734,22 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({ |
|
|
|
|
|
|
|
// Provider settings actions
|
|
|
|
updateProviderSettings: (settings: ProviderSettings) => { |
|
|
|
set({ providerSettings: settings }); |
|
|
|
const promoted = promoteUnavailableGeminiFallbacks(get().nodes, settings, get().providerEnvStatus); |
|
|
|
set({ |
|
|
|
providerSettings: settings, |
|
|
|
...(promoted.changed ? { nodes: promoted.nodes, hasUnsavedChanges: true } : {}), |
|
|
|
}); |
|
|
|
saveProviderSettings(settings); |
|
|
|
}, |
|
|
|
|
|
|
|
updateProviderEnvStatus: (status: ProviderEnvStatus) => { |
|
|
|
const promoted = promoteUnavailableGeminiFallbacks(get().nodes, get().providerSettings, status); |
|
|
|
set({ |
|
|
|
providerEnvStatus: status, |
|
|
|
...(promoted.changed ? { nodes: promoted.nodes, hasUnsavedChanges: true } : {}), |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
updateProviderApiKey: (providerId: ProviderType, apiKey: string | null) => { |
|
|
|
const { providerSettings } = get(); |
|
|
|
const updated: ProviderSettings = { |
|
|
|
|