Browse Source

feat: add Image Search toggle for Nano Banana 2

Nano Banana 2 (gemini-3.1-flash-image) supports a new searchTypes API
that allows enabling web search and image search independently. This adds
a useImageSearch toggle visible only for Nano Banana 2, while keeping the
existing Google Search toggle using the legacy format for Nano Banana Pro.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
cbc278f801
  1. 95
      src/app/api/generate/__tests__/route.test.ts
  2. 11
      src/app/api/generate/providers/gemini.ts
  3. 4
      src/app/api/generate/route.ts
  4. 18
      src/components/SplitGridSettingsModal.tsx
  5. 21
      src/components/nodes/GenerateImageNode.tsx
  6. 4
      src/lib/chat/contextBuilder.test.ts
  7. 1
      src/lib/chat/editOperations.test.ts
  8. 1
      src/lib/quickstart/templates.ts
  9. 2
      src/lib/quickstart/validation.ts
  10. 1
      src/store/execution/__tests__/nanoBananaExecutor.test.ts
  11. 1
      src/store/execution/nanoBananaExecutor.ts
  12. 2
      src/store/utils/__tests__/localStorage.test.ts
  13. 2
      src/store/utils/localStorage.ts
  14. 3
      src/store/utils/nodeDefaults.ts
  15. 3
      src/types/api.ts
  16. 5
      src/types/nodes.ts

95
src/app/api/generate/__tests__/route.test.ts

@ -298,6 +298,101 @@ describe("/api/generate route", () => {
);
});
it("should apply imageSearch searchType for nano-banana-2 with useImageSearch only", async () => {
process.env.GEMINI_API_KEY = "test-gemini-key";
mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse());
const request = createMockPostRequest({
prompt: "Find similar images",
model: "nano-banana-2",
useImageSearch: true,
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(mockGenerateContent).toHaveBeenCalledWith(
expect.objectContaining({
tools: [{ googleSearch: { searchTypes: { imageSearch: {} } } }],
})
);
});
it("should apply both webSearch and imageSearch for nano-banana-2 with both toggles", async () => {
process.env.GEMINI_API_KEY = "test-gemini-key";
mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse());
const request = createMockPostRequest({
prompt: "Latest technology with images",
model: "nano-banana-2",
useGoogleSearch: true,
useImageSearch: true,
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(mockGenerateContent).toHaveBeenCalledWith(
expect.objectContaining({
tools: [{ googleSearch: { searchTypes: { webSearch: {}, imageSearch: {} } } }],
})
);
});
it("should use webSearch searchType for nano-banana-2 with useGoogleSearch only", async () => {
process.env.GEMINI_API_KEY = "test-gemini-key";
mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse());
const request = createMockPostRequest({
prompt: "Latest technology trends",
model: "nano-banana-2",
useGoogleSearch: true,
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(mockGenerateContent).toHaveBeenCalledWith(
expect.objectContaining({
tools: [{ googleSearch: { searchTypes: { webSearch: {} } } }],
})
);
});
it("should ignore useImageSearch for nano-banana-pro", async () => {
process.env.GEMINI_API_KEY = "test-gemini-key";
mockGenerateContent.mockResolvedValueOnce(createGeminiImageResponse());
const request = createMockPostRequest({
prompt: "Test prompt",
model: "nano-banana-pro",
useGoogleSearch: true,
useImageSearch: true,
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
// nano-banana-pro should use old format, ignoring useImageSearch
expect(mockGenerateContent).toHaveBeenCalledWith(
expect.objectContaining({
tools: [{ googleSearch: {} }],
})
);
});
it("should use nano-banana-pro as default model", async () => {
process.env.GEMINI_API_KEY = "test-gemini-key";

11
src/app/api/generate/providers/gemini.ts

@ -28,7 +28,8 @@ export async function generateWithGemini(
model: ModelType,
aspectRatio?: string,
resolution?: string,
useGoogleSearch?: boolean
useGoogleSearch?: boolean,
useImageSearch?: boolean
): Promise<NextResponse<GenerateResponse>> {
console.log(`[API:${requestId}] Gemini generation - Model: ${model}, Images: ${images?.length || 0}, Prompt: ${prompt?.length || 0} chars`);
@ -82,7 +83,13 @@ export async function generateWithGemini(
// Add tools array for Google Search (Nano Banana Pro and Nano Banana 2)
const tools = [];
if ((model === "nano-banana-pro" || model === "nano-banana-2") && useGoogleSearch) {
if (model === "nano-banana-2" && (useGoogleSearch || useImageSearch)) {
// Nano Banana 2 uses searchTypes to enable web and/or image search independently
const searchTypes: Record<string, Record<string, never>> = {};
if (useGoogleSearch) searchTypes.webSearch = {};
if (useImageSearch) searchTypes.imageSearch = {};
tools.push({ googleSearch: { searchTypes } });
} else if (model === "nano-banana-pro" && useGoogleSearch) {
tools.push({ googleSearch: {} });
}

4
src/app/api/generate/route.ts

@ -95,6 +95,7 @@ export async function POST(request: NextRequest) {
aspectRatio,
resolution,
useGoogleSearch,
useImageSearch,
selectedModel,
parameters,
dynamicInputs,
@ -485,7 +486,8 @@ export async function POST(request: NextRequest) {
geminiModel,
aspectRatio,
resolution,
useGoogleSearch
useGoogleSearch,
useImageSearch
);
} catch (error) {
// Extract error information

18
src/components/SplitGridSettingsModal.tsx

@ -49,6 +49,7 @@ export function SplitGridSettingsModal({
const [resolution, setResolution] = useState(nodeData.generateSettings.resolution);
const [model, setModel] = useState(nodeData.generateSettings.model);
const [useGoogleSearch, setUseGoogleSearch] = useState(nodeData.generateSettings.useGoogleSearch);
const [useImageSearch, setUseImageSearch] = useState(nodeData.generateSettings.useImageSearch);
const { rows, cols } = LAYOUT_OPTIONS[selectedLayoutIndex];
const targetCount = rows * cols;
@ -108,6 +109,7 @@ export function SplitGridSettingsModal({
resolution,
model,
useGoogleSearch,
useImageSearch,
});
// Create prompt node (below imageInput)
@ -158,6 +160,7 @@ export function SplitGridSettingsModal({
resolution,
model,
useGoogleSearch,
useImageSearch,
},
childNodeIds,
gridRows: rows,
@ -168,7 +171,7 @@ export function SplitGridSettingsModal({
onClose();
}, [
nodeId, targetCount, defaultPrompt, aspectRatio, resolution,
model, useGoogleSearch, rows, cols, selectedLayoutIndex, getNodeById,
model, useGoogleSearch, useImageSearch, rows, cols, selectedLayoutIndex, getNodeById,
addNode, updateNodeData, onConnect, addEdgeWithType, onClose
]);
@ -316,6 +319,19 @@ export function SplitGridSettingsModal({
Google Search
</label>
</div>
{model === "nano-banana-2" && (
<div className="flex items-end pb-2">
<label className="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
<input
type="checkbox"
checked={useImageSearch}
onChange={(e) => setUseImageSearch(e.target.checked)}
className="w-4 h-4 rounded border-neutral-600 bg-neutral-900"
/>
Image Search
</label>
</div>
)}
</>
)}
</div>

21
src/components/nodes/GenerateImageNode.tsx

@ -233,6 +233,15 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
[id, updateNodeData]
);
const handleImageSearchToggle = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const useImageSearch = e.target.checked;
updateNodeData(id, { useImageSearch });
saveNanoBananaDefaults({ useImageSearch });
},
[id, updateNodeData]
);
const handleParametersChange = useCallback(
(parameters: Record<string, unknown>) => {
updateNodeData(id, { parameters });
@ -727,6 +736,18 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
<span>Google Search</span>
</label>
)}
{/* Image Search toggle - only for Nano Banana 2 */}
{currentProvider === "gemini" && currentModelId === "nano-banana-2" && (
<label className="flex items-center gap-1.5 text-[10px] text-neutral-300 shrink-0 cursor-pointer">
<input
type="checkbox"
checked={nodeData.useImageSearch}
onChange={handleImageSearchToggle}
className="w-3 h-3 rounded border-neutral-700 bg-neutral-900/50 text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/>
<span>Image Search</span>
</label>
)}
</div>
</BaseNode>

4
src/lib/chat/contextBuilder.test.ts

@ -239,6 +239,7 @@ describe("stripBinaryData", () => {
resolution: "1K",
model: "nano-banana",
useGoogleSearch: false,
useImageSearch: false,
},
childNodeIds: [],
gridRows: 2,
@ -258,6 +259,7 @@ describe("stripBinaryData", () => {
resolution: "1K",
model: "nano-banana",
useGoogleSearch: false,
useImageSearch: false,
});
expect(stripped.data.sourceImage).toMatch(/\[image:.*KB\]/);
});
@ -373,7 +375,7 @@ describe("stripBinaryData", () => {
sourceImage: FAKE_BASE64_PNG,
targetCount: 4,
defaultPrompt: "test",
generateSettings: { aspectRatio: "1:1", resolution: "1K", model: "nano-banana", useGoogleSearch: false },
generateSettings: { aspectRatio: "1:1", resolution: "1K", model: "nano-banana", useGoogleSearch: false, useImageSearch: false },
childNodeIds: [],
gridRows: 2,
gridCols: 2,

1
src/lib/chat/editOperations.test.ts

@ -24,6 +24,7 @@ vi.mock("@/store/utils/nodeDefaults", () => ({
resolution: "1K",
model: "nano-banana",
useGoogleSearch: false,
useImageSearch: false,
status: "idle",
error: null,
imageHistory: [],

1
src/lib/quickstart/templates.ts

@ -82,6 +82,7 @@ const createNanoBananaData = () => ({
resolution: "1K" as const,
model: "nano-banana-pro" as const,
useGoogleSearch: false,
useImageSearch: false,
status: "idle" as const,
error: null,
imageHistory: [],

2
src/lib/quickstart/validation.ts

@ -264,6 +264,7 @@ function createDefaultNodeData(type: NodeType): WorkflowNodeData {
resolution: "1K",
model: "nano-banana-pro",
useGoogleSearch: false,
useImageSearch: false,
status: "idle",
error: null,
imageHistory: [],
@ -325,6 +326,7 @@ function createDefaultNodeData(type: NodeType): WorkflowNodeData {
resolution: "1K",
model: "nano-banana-pro",
useGoogleSearch: false,
useImageSearch: false,
},
childNodeIds: [],
gridRows: 2,

1
src/store/execution/__tests__/nanoBananaExecutor.test.ts

@ -27,6 +27,7 @@ function makeNode(data: Record<string, unknown> = {}): WorkflowNode {
resolution: "1024x1024",
model: "nano-banana",
useGoogleSearch: false,
useImageSearch: false,
selectedModel: { provider: "gemini", modelId: "nano-banana", displayName: "Nano Banana" },
parameters: {},
imageHistory: [],

1
src/store/execution/nanoBananaExecutor.ts

@ -101,6 +101,7 @@ export async function executeNanoBanana(
resolution: nodeData.resolution,
model: nodeData.model,
useGoogleSearch: nodeData.useGoogleSearch,
useImageSearch: nodeData.useImageSearch,
selectedModel: nodeData.selectedModel,
parameters: nodeData.parameters,
dynamicInputs: sanitizedDynamicInputs,

2
src/store/utils/__tests__/localStorage.test.ts

@ -171,6 +171,7 @@ describe("localStorage utilities", () => {
resolution: "1K",
model: "nano-banana-pro",
useGoogleSearch: false,
useImageSearch: false,
});
});
@ -199,6 +200,7 @@ describe("localStorage utilities", () => {
resolution: "1K",
model: "nano-banana-pro",
useGoogleSearch: false,
useImageSearch: false,
});
});
});

2
src/store/utils/localStorage.ts

@ -29,6 +29,7 @@ export interface GenerateImageDefaults {
resolution: string;
model: string;
useGoogleSearch: boolean;
useImageSearch: boolean;
}
const DEFAULT_GENERATE_IMAGE_SETTINGS: GenerateImageDefaults = {
@ -36,6 +37,7 @@ const DEFAULT_GENERATE_IMAGE_SETTINGS: GenerateImageDefaults = {
resolution: "1K",
model: "nano-banana-pro",
useGoogleSearch: false,
useImageSearch: false,
};
// Default provider settings

3
src/store/utils/nodeDefaults.ts

@ -141,6 +141,7 @@ export const createDefaultNodeData = (type: NodeType): WorkflowNodeData => {
const aspectRatio = nodeDefaults.generateImage?.aspectRatio ?? legacyDefaults.aspectRatio;
const resolution = nodeDefaults.generateImage?.resolution ?? legacyDefaults.resolution;
const useGoogleSearch = nodeDefaults.generateImage?.useGoogleSearch ?? legacyDefaults.useGoogleSearch;
const useImageSearch = nodeDefaults.generateImage?.useImageSearch ?? legacyDefaults.useImageSearch;
return {
inputImages: [],
@ -151,6 +152,7 @@ export const createDefaultNodeData = (type: NodeType): WorkflowNodeData => {
model: legacyDefaults.model, // Keep legacy model field for backward compat
selectedModel,
useGoogleSearch,
useImageSearch,
status: "idle",
error: null,
imageHistory: [],
@ -222,6 +224,7 @@ export const createDefaultNodeData = (type: NodeType): WorkflowNodeData => {
resolution: "1K",
model: "nano-banana-pro",
useGoogleSearch: false,
useImageSearch: false,
},
childNodeIds: [],
gridRows: 2,

3
src/types/api.ts

@ -15,7 +15,8 @@ export interface GenerateRequest {
aspectRatio?: AspectRatio;
resolution?: Resolution; // Only for Nano Banana Pro
model?: ModelType;
useGoogleSearch?: boolean; // Only for Nano Banana Pro
useGoogleSearch?: boolean; // Only for Nano Banana Pro and Nano Banana 2
useImageSearch?: boolean; // Only for Nano Banana 2
mediaType?: "image" | "video" | "3d" | "audio"; // Indicates expected output type for provider routing
}

5
src/types/nodes.ts

@ -170,7 +170,8 @@ export interface NanoBananaNodeData extends BaseNodeData {
resolution: Resolution; // Only used by Nano Banana Pro
model: ModelType;
selectedModel?: SelectedModel; // Multi-provider model selection (optional for backward compat)
useGoogleSearch: boolean; // Only available for Nano Banana Pro
useGoogleSearch: boolean; // Only available for Nano Banana Pro and Nano Banana 2
useImageSearch: boolean; // Only available for Nano Banana 2
parameters?: Record<string, unknown>; // Model-specific parameters for external providers
inputSchema?: ModelInputDef[]; // Model's input schema for dynamic handles
status: NodeStatus;
@ -362,6 +363,7 @@ export interface SplitGridNodeData extends BaseNodeData {
resolution: Resolution;
model: ModelType;
useGoogleSearch: boolean;
useImageSearch: boolean;
};
childNodeIds: Array<{
imageInput: string;
@ -433,6 +435,7 @@ export interface GenerateImageNodeDefaults {
aspectRatio?: string;
resolution?: string;
useGoogleSearch?: boolean;
useImageSearch?: boolean;
}
export interface GenerateVideoNodeDefaults {

Loading…
Cancel
Save