Browse Source

feat: add nano-banana-2 to model registry, UI, and migration paths

- Add Nano Banana 2 entry to GEMINI_IMAGE_MODELS in model registry
- Add model to GenerateImageNode dropdown with 512px resolution option
- Extend resolution/Google Search gates to support nano-banana-2
- Replace hardcoded model name ternaries with MODEL_DISPLAY_NAMES lookup
  in workflowStore, nodeDefaults, and GenerateImageNode migration code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
4fd01a26bf
  1. 9
      src/app/api/models/route.ts
  2. 17
      src/components/nodes/GenerateImageNode.tsx
  3. 3
      src/store/utils/nodeDefaults.ts
  4. 3
      src/store/workflowStore.ts

9
src/app/api/models/route.ts

@ -366,6 +366,15 @@ const GEMINI_IMAGE_MODELS: ProviderModel[] = [
coverImage: undefined, coverImage: undefined,
pricing: { type: "per-run", amount: 0.039, currency: "USD" }, pricing: { type: "per-run", amount: 0.039, currency: "USD" },
}, },
{
id: "nano-banana-2",
name: "Nano Banana 2",
description: "High-efficiency image generation with Gemini 3.1 Flash. Supports resolution control (512/1K/2K/4K), Google Search grounding, and up to 10 reference images.",
provider: "gemini",
capabilities: ["text-to-image", "image-to-image"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.067, currency: "USD" },
},
{ {
id: "nano-banana-pro", id: "nano-banana-pro",
name: "Nano Banana Pro", name: "Nano Banana Pro",

17
src/components/nodes/GenerateImageNode.tsx

@ -7,7 +7,7 @@ import { useCommentNavigation } from "@/hooks/useCommentNavigation";
import { ModelParameters } from "./ModelParameters"; import { ModelParameters } from "./ModelParameters";
import { useWorkflowStore, saveNanoBananaDefaults, useProviderApiKeys } from "@/store/workflowStore"; import { useWorkflowStore, saveNanoBananaDefaults, useProviderApiKeys } from "@/store/workflowStore";
import { deduplicatedFetch } from "@/utils/deduplicatedFetch"; import { deduplicatedFetch } from "@/utils/deduplicatedFetch";
import { NanoBananaNodeData, AspectRatio, Resolution, ModelType, ProviderType, SelectedModel, ModelInputDef } from "@/types"; import { NanoBananaNodeData, AspectRatio, Resolution, ModelType, MODEL_DISPLAY_NAMES, ProviderType, SelectedModel, ModelInputDef } from "@/types";
import { ProviderModel, ModelCapability } from "@/lib/providers/types"; import { ProviderModel, ModelCapability } from "@/lib/providers/types";
import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog";
import { useToast } from "@/components/Toast"; import { useToast } from "@/components/Toast";
@ -17,12 +17,13 @@ import { ProviderBadge } from "./ProviderBadge";
// All 10 aspect ratios supported by both models // All 10 aspect ratios supported by both models
const ASPECT_RATIOS: AspectRatio[] = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]; const ASPECT_RATIOS: AspectRatio[] = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
// Resolutions only for Nano Banana Pro (gemini-3-pro-image-preview) // Resolutions for Nano Banana Pro and Nano Banana 2
const RESOLUTIONS: Resolution[] = ["1K", "2K", "4K"]; const RESOLUTIONS: Resolution[] = ["512", "1K", "2K", "4K"];
// Hardcoded Gemini image models (always available) // Hardcoded Gemini image models (always available)
const GEMINI_IMAGE_MODELS: { value: ModelType; label: string }[] = [ const GEMINI_IMAGE_MODELS: { value: ModelType; label: string }[] = [
{ value: "nano-banana", label: "Nano Banana" }, { value: "nano-banana", label: "Nano Banana" },
{ value: "nano-banana-2", label: "Nano Banana 2" },
{ value: "nano-banana-pro", label: "Nano Banana Pro" }, { value: "nano-banana-pro", label: "Nano Banana Pro" },
]; ];
@ -78,7 +79,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
// Migrate legacy data: derive selectedModel from model field if missing // Migrate legacy data: derive selectedModel from model field if missing
useEffect(() => { useEffect(() => {
if (nodeData.model && !nodeData.selectedModel) { if (nodeData.model && !nodeData.selectedModel) {
const displayName = nodeData.model === "nano-banana" ? "Nano Banana" : "Nano Banana Pro"; const displayName = MODEL_DISPLAY_NAMES[nodeData.model] || nodeData.model;
const newSelectedModel: SelectedModel = { const newSelectedModel: SelectedModel = {
provider: "gemini", provider: "gemini",
modelId: nodeData.model, modelId: nodeData.model,
@ -392,7 +393,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
}, [isGeminiOnly]); }, [isGeminiOnly]);
// Use selectedModel.modelId for Gemini models, fallback to legacy model field // Use selectedModel.modelId for Gemini models, fallback to legacy model field
const currentModelId = isGeminiProvider ? (nodeData.selectedModel?.modelId || nodeData.model) : null; const currentModelId = isGeminiProvider ? (nodeData.selectedModel?.modelId || nodeData.model) : null;
const isNanoBananaPro = currentModelId === "nano-banana-pro"; const supportsResolution = currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2";
const hasCarouselImages = (nodeData.imageHistory || []).length > 1; const hasCarouselImages = (nodeData.imageHistory || []).length > 1;
// Track previous status to detect error transitions // Track previous status to detect error transitions
@ -694,7 +695,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
</option> </option>
))} ))}
</select> </select>
{isNanoBananaPro && ( {supportsResolution && (
<select <select
value={nodeData.resolution} value={nodeData.resolution}
onChange={handleResolutionChange} onChange={handleResolutionChange}
@ -710,8 +711,8 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
</div> </div>
)} )}
{/* Google Search toggle - only for Nano Banana Pro */} {/* Google Search toggle - for Nano Banana Pro and Nano Banana 2 */}
{currentProvider === "gemini" && isNanoBananaPro && ( {currentProvider === "gemini" && supportsResolution && (
<label className="flex items-center gap-1.5 text-[10px] text-neutral-300 shrink-0 cursor-pointer"> <label className="flex items-center gap-1.5 text-[10px] text-neutral-300 shrink-0 cursor-pointer">
<input <input
type="checkbox" type="checkbox"

3
src/store/utils/nodeDefaults.ts

@ -22,6 +22,7 @@ import {
WorkflowNodeData, WorkflowNodeData,
GroupColor, GroupColor,
SelectedModel, SelectedModel,
MODEL_DISPLAY_NAMES,
} from "@/types"; } from "@/types";
import { loadGenerateImageDefaults, loadNodeDefaults } from "./localStorage"; import { loadGenerateImageDefaults, loadNodeDefaults } from "./localStorage";
@ -127,7 +128,7 @@ export const createDefaultNodeData = (type: NodeType): WorkflowNodeData => {
if (nodeDefaults.generateImage?.selectedModel) { if (nodeDefaults.generateImage?.selectedModel) {
selectedModel = nodeDefaults.generateImage.selectedModel; selectedModel = nodeDefaults.generateImage.selectedModel;
} else { } else {
const modelDisplayName = legacyDefaults.model === "nano-banana" ? "Nano Banana" : "Nano Banana Pro"; const modelDisplayName = MODEL_DISPLAY_NAMES[legacyDefaults.model] || legacyDefaults.model;
selectedModel = { selectedModel = {
provider: "gemini", provider: "gemini",
modelId: legacyDefaults.model, modelId: legacyDefaults.model,

3
src/store/workflowStore.ts

@ -23,6 +23,7 @@ import {
ProviderSettings, ProviderSettings,
RecentModel, RecentModel,
CanvasNavigationSettings, CanvasNavigationSettings,
MODEL_DISPLAY_NAMES,
} from "@/types"; } from "@/types";
import { useToast } from "@/components/Toast"; import { useToast } from "@/components/Toast";
import { logger } from "@/utils/logger"; import { logger } from "@/utils/logger";
@ -1491,7 +1492,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
if (node.type === "nanoBanana") { if (node.type === "nanoBanana") {
const data = node.data as NanoBananaNodeData; const data = node.data as NanoBananaNodeData;
if (data.model && !data.selectedModel) { if (data.model && !data.selectedModel) {
const displayName = data.model === "nano-banana" ? "Nano Banana" : "Nano Banana Pro"; const displayName = MODEL_DISPLAY_NAMES[data.model] || data.model;
return { return {
...node, ...node,
data: { data: {

Loading…
Cancel
Save