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,
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",
name: "Nano Banana Pro",

17
src/components/nodes/GenerateImageNode.tsx

@ -7,7 +7,7 @@ import { useCommentNavigation } from "@/hooks/useCommentNavigation";
import { ModelParameters } from "./ModelParameters";
import { useWorkflowStore, saveNanoBananaDefaults, useProviderApiKeys } from "@/store/workflowStore";
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 { ModelSearchDialog } from "@/components/modals/ModelSearchDialog";
import { useToast } from "@/components/Toast";
@ -17,12 +17,13 @@ import { ProviderBadge } from "./ProviderBadge";
// 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"];
// Resolutions only for Nano Banana Pro (gemini-3-pro-image-preview)
const RESOLUTIONS: Resolution[] = ["1K", "2K", "4K"];
// Resolutions for Nano Banana Pro and Nano Banana 2
const RESOLUTIONS: Resolution[] = ["512", "1K", "2K", "4K"];
// Hardcoded Gemini image models (always available)
const GEMINI_IMAGE_MODELS: { value: ModelType; label: string }[] = [
{ value: "nano-banana", label: "Nano Banana" },
{ value: "nano-banana-2", label: "Nano Banana 2" },
{ 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
useEffect(() => {
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 = {
provider: "gemini",
modelId: nodeData.model,
@ -392,7 +393,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
}, [isGeminiOnly]);
// Use selectedModel.modelId for Gemini models, fallback to legacy model field
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;
// Track previous status to detect error transitions
@ -694,7 +695,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
</option>
))}
</select>
{isNanoBananaPro && (
{supportsResolution && (
<select
value={nodeData.resolution}
onChange={handleResolutionChange}
@ -710,8 +711,8 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
</div>
)}
{/* Google Search toggle - only for Nano Banana Pro */}
{currentProvider === "gemini" && isNanoBananaPro && (
{/* Google Search toggle - for Nano Banana Pro and Nano Banana 2 */}
{currentProvider === "gemini" && supportsResolution && (
<label className="flex items-center gap-1.5 text-[10px] text-neutral-300 shrink-0 cursor-pointer">
<input
type="checkbox"

3
src/store/utils/nodeDefaults.ts

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

3
src/store/workflowStore.ts

@ -23,6 +23,7 @@ import {
ProviderSettings,
RecentModel,
CanvasNavigationSettings,
MODEL_DISPLAY_NAMES,
} from "@/types";
import { useToast } from "@/components/Toast";
import { logger } from "@/utils/logger";
@ -1491,7 +1492,7 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
if (node.type === "nanoBanana") {
const data = node.data as NanoBananaNodeData;
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 {
...node,
data: {

Loading…
Cancel
Save