From ad4779884f3b6c93347199e1f163b6e4bc5995c2 Mon Sep 17 00:00:00 2001 From: jiajia Date: Tue, 19 May 2026 10:44:30 +0800 Subject: [PATCH] Stop duplicate image settings from competing with composer The bottom composer is the canonical image-generation control surface now. The legacy right-side control panel was still rendering schema defaults for selected image nodes, which made aspect and size controls disagree on the same operation. Constraint: Image generation controls must stay consistent with the Popi composer UX Rejected: Synchronize both panels | duplicating controls preserves the confusing two-source UI Confidence: high Scope-risk: narrow Directive: Do not re-enable the right-side image panel unless it reads from the same composer-owned state Tested: npm run test:run -- src/components/__tests__/ControlPanel.test.tsx Tested: npm run test:run -- src/components/__tests__/GenerationComposer.test.tsx src/components/__tests__/ControlPanel.test.tsx Tested: PROVIDER_MODE=popi NEXT_PUBLIC_PROVIDER_MODE=popi npm run build Tested: Playwright localhost check for composer visible and legacy image settings absent --- .../__tests__/ControlPanel.test.tsx | 101 ++++++++++++++++++ src/components/nodes/ControlPanel.tsx | 10 ++ 2 files changed, 111 insertions(+) create mode 100644 src/components/__tests__/ControlPanel.test.tsx diff --git a/src/components/__tests__/ControlPanel.test.tsx b/src/components/__tests__/ControlPanel.test.tsx new file mode 100644 index 00000000..fbea6ff1 --- /dev/null +++ b/src/components/__tests__/ControlPanel.test.tsx @@ -0,0 +1,101 @@ +import { render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ControlPanel } from "@/components/nodes/ControlPanel"; +import { useWorkflowStore } from "@/store/workflowStore"; +import type { WorkflowNode } from "@/types"; + +vi.mock("@/hooks/useInlineParameters", () => ({ + useInlineParameters: () => ({ inlineParametersEnabled: false }), +})); + +vi.mock("@/components/nodes/ModelParameters", () => ({ + ModelParameters: () =>
, +})); + +vi.mock("@/components/modals/ModelSearchDialog", () => ({ + ModelSearchDialog: () => null, +})); + +function imageNode(): WorkflowNode { + return { + id: "image-1", + type: "nanoBanana", + selected: true, + position: { x: 0, y: 0 }, + data: { + inputImages: [], + inputPrompt: "coffee spilled", + outputImage: null, + aspectRatio: "16:9", + resolution: "1K", + model: "nano-banana-pro", + selectedModel: { + provider: "newapiwg", + modelId: "apiyi_nano_banana_2", + displayName: "apiyi_nano_banana_2", + capabilities: ["text-to-image", "image-to-image"], + }, + parameters: { quality: "auto", size: "1024x1024" }, + useGoogleSearch: false, + useImageSearch: false, + status: "idle", + error: null, + imageHistory: [], + selectedHistoryIndex: 0, + }, + } as WorkflowNode; +} + +function videoNode(): WorkflowNode { + return { + id: "video-1", + type: "generateVideo", + selected: true, + position: { x: 0, y: 0 }, + data: { + prompt: "camera pan", + inputImage: null, + outputVideo: null, + selectedModel: { + provider: "newapiwg", + modelId: "video-model", + displayName: "Video Model", + capabilities: ["text-to-video"], + }, + parameters: {}, + status: "idle", + error: null, + progress: 0, + }, + } as WorkflowNode; +} + +describe("ControlPanel", () => { + beforeEach(() => { + vi.clearAllMocks(); + useWorkflowStore.setState({ + nodes: [], + edges: [], + isRunning: false, + }); + }); + + it("does not render duplicate settings for image generation nodes controlled by the composer", () => { + useWorkflowStore.setState({ nodes: [imageNode()] }); + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + expect(screen.queryByText("Generate Image Settings")).not.toBeInTheDocument(); + expect(screen.queryByText("1024x1024")).not.toBeInTheDocument(); + }); + + it("keeps the right-side panel for generation nodes not yet owned by the composer", () => { + useWorkflowStore.setState({ nodes: [videoNode()] }); + + render(); + + expect(screen.getByText("Generate Video Settings")).toBeInTheDocument(); + expect(screen.getByTestId("model-parameters")).toBeInTheDocument(); + }); +}); diff --git a/src/components/nodes/ControlPanel.tsx b/src/components/nodes/ControlPanel.tsx index 5987a0f8..8c5e2fdb 100644 --- a/src/components/nodes/ControlPanel.tsx +++ b/src/components/nodes/ControlPanel.tsx @@ -39,6 +39,10 @@ const GENERATION_NODE_TYPES: NodeType[] = [ "llmGenerate", ]; +const COMPOSER_CONTROLLED_NODE_TYPES: NodeType[] = [ + "nanoBanana", +]; + // Base 10 aspect ratios (all Gemini image models) const BASE_ASPECT_RATIOS: AspectRatio[] = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]; @@ -101,6 +105,12 @@ export function ControlPanel() { return null; } + // Image generation settings live in the bottom composer. Keeping the old + // right-side panel visible creates duplicate controls with separate defaults. + if (COMPOSER_CONTROLLED_NODE_TYPES.includes(selectedNode.type as NodeType)) { + return null; + } + // Check if this is a generation node const isGenerationNode = selectedNode && GENERATION_NODE_TYPES.includes(selectedNode.type as NodeType);