diff --git a/src/app/api/generate/__tests__/route.test.ts b/src/app/api/generate/__tests__/route.test.ts index 631928b9..b4a3e904 100644 --- a/src/app/api/generate/__tests__/route.test.ts +++ b/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"; diff --git a/src/app/api/generate/providers/gemini.ts b/src/app/api/generate/providers/gemini.ts index b725e309..b7efd404 100644 --- a/src/app/api/generate/providers/gemini.ts +++ b/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> { 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> = {}; + if (useGoogleSearch) searchTypes.webSearch = {}; + if (useImageSearch) searchTypes.imageSearch = {}; + tools.push({ googleSearch: { searchTypes } }); + } else if (model === "nano-banana-pro" && useGoogleSearch) { tools.push({ googleSearch: {} }); } diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index 051c59bd..1841de53 100644 --- a/src/app/api/generate/route.ts +++ b/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 diff --git a/src/components/SplitGridSettingsModal.tsx b/src/components/SplitGridSettingsModal.tsx index c26ff4ab..c06f31fe 100644 --- a/src/components/SplitGridSettingsModal.tsx +++ b/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 + {model === "nano-banana-2" && ( +
+ +
+ )} )} diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index da306607..b767e764 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -233,6 +233,15 @@ export function GenerateImageNode({ id, data, selected }: NodeProps) => { + const useImageSearch = e.target.checked; + updateNodeData(id, { useImageSearch }); + saveNanoBananaDefaults({ useImageSearch }); + }, + [id, updateNodeData] + ); + const handleParametersChange = useCallback( (parameters: Record) => { updateNodeData(id, { parameters }); @@ -727,6 +736,18 @@ export function GenerateImageNode({ id, data, selected }: NodePropsGoogle Search )} + {/* Image Search toggle - only for Nano Banana 2 */} + {currentProvider === "gemini" && currentModelId === "nano-banana-2" && ( + + )} diff --git a/src/lib/chat/contextBuilder.test.ts b/src/lib/chat/contextBuilder.test.ts index f3fa5a61..caa4879d 100644 --- a/src/lib/chat/contextBuilder.test.ts +++ b/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, diff --git a/src/lib/chat/editOperations.test.ts b/src/lib/chat/editOperations.test.ts index 1ce9d1b3..6e5fa59c 100644 --- a/src/lib/chat/editOperations.test.ts +++ b/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: [], diff --git a/src/lib/quickstart/templates.ts b/src/lib/quickstart/templates.ts index 51533c68..7881823d 100644 --- a/src/lib/quickstart/templates.ts +++ b/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: [], diff --git a/src/lib/quickstart/validation.ts b/src/lib/quickstart/validation.ts index 4afce636..38f4e813 100644 --- a/src/lib/quickstart/validation.ts +++ b/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, diff --git a/src/store/execution/__tests__/nanoBananaExecutor.test.ts b/src/store/execution/__tests__/nanoBananaExecutor.test.ts index bf31cca8..8f4d94c2 100644 --- a/src/store/execution/__tests__/nanoBananaExecutor.test.ts +++ b/src/store/execution/__tests__/nanoBananaExecutor.test.ts @@ -27,6 +27,7 @@ function makeNode(data: Record = {}): WorkflowNode { resolution: "1024x1024", model: "nano-banana", useGoogleSearch: false, + useImageSearch: false, selectedModel: { provider: "gemini", modelId: "nano-banana", displayName: "Nano Banana" }, parameters: {}, imageHistory: [], diff --git a/src/store/execution/nanoBananaExecutor.ts b/src/store/execution/nanoBananaExecutor.ts index 29f6e29b..bd142993 100644 --- a/src/store/execution/nanoBananaExecutor.ts +++ b/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, diff --git a/src/store/utils/__tests__/localStorage.test.ts b/src/store/utils/__tests__/localStorage.test.ts index c03c287e..a16b0cb7 100644 --- a/src/store/utils/__tests__/localStorage.test.ts +++ b/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, }); }); }); diff --git a/src/store/utils/localStorage.ts b/src/store/utils/localStorage.ts index eb9500eb..41165034 100644 --- a/src/store/utils/localStorage.ts +++ b/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 diff --git a/src/store/utils/nodeDefaults.ts b/src/store/utils/nodeDefaults.ts index 84d079c1..777baac7 100644 --- a/src/store/utils/nodeDefaults.ts +++ b/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, diff --git a/src/types/api.ts b/src/types/api.ts index 080da5bc..2173c8a0 100644 --- a/src/types/api.ts +++ b/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 } diff --git a/src/types/nodes.ts b/src/types/nodes.ts index 2c23b525..8851d2eb 100644 --- a/src/types/nodes.ts +++ b/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; // 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 {