diff --git a/src/components/__tests__/GenerationComposer.test.tsx b/src/components/__tests__/GenerationComposer.test.tsx index 6215587f..687002fb 100644 --- a/src/components/__tests__/GenerationComposer.test.tsx +++ b/src/components/__tests__/GenerationComposer.test.tsx @@ -1046,13 +1046,19 @@ describe("GenerationComposer", () => { expect(data.durationSeconds).toBe(4); }); - it("shows custom selected video duration in quick settings", () => { + it("defaults gateway video dimension quick settings to the first schema option", async () => { useWorkflowStore.setState({ nodes: [videoNode("vid-1", true, { durationSeconds: 6 })], }); render(); - expect(screen.getByLabelText("Duration")).toHaveValue("6"); + expect(screen.getByLabelText("Duration")).toHaveValue("4"); + await waitFor(() => { + expect((useWorkflowStore.getState().nodes[0].data as GenerateVideoNodeData).parameters).toMatchObject({ + duration: "4", + resolution: "720p", + }); + }); }); it("uses the audio model filter and reruns the selected audio node", async () => { diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index ca63d6c2..b5289d95 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -208,10 +208,6 @@ function buildGatewayDimensionSelects( .map((parameter) => { const options = getSchemaSelectOptions(parameter); const value = draft.parameters?.[parameter.name] - ?? (parameter.name === "ratio" ? draft.aspectRatio : undefined) - ?? (parameter.name === "resolution" ? draft.resolution : undefined) - ?? (parameter.name === "duration" ? draft.durationSeconds : undefined) - ?? parameter.default ?? options[0]?.value; return { parameter, @@ -221,6 +217,24 @@ function buildGatewayDimensionSelects( }); } +function applyGatewayDimensionParameterDefaults( + schema: ModelParameter[] | undefined, + parameters: Record | undefined +): { parameters: Record; changed: boolean } { + const next = { ...(parameters ?? {}) }; + let changed = false; + const nameSet = new Set(GATEWAY_DIMENSION_PARAMETER_NAMES); + + for (const parameter of schema ?? []) { + if (!nameSet.has(parameter.name) || !parameter.options || parameter.options.length === 0) continue; + if (next[parameter.name] !== undefined && next[parameter.name] !== null && next[parameter.name] !== "") continue; + next[parameter.name] = parameter.options[0]?.value; + changed = true; + } + + return { parameters: next, changed }; +} + function getImageCountOptions(schema: ModelParameter[] | undefined): ImageGenerationCount[] { return getSchemaOptionValues(schema, IMAGE_COUNT_PARAMETER_NAMES) .map((value) => Number(value)) @@ -943,7 +957,7 @@ function buildInitialDataForNode(nodeType: NodeType, draft: ComposerDraft): Part model: getLegacyImageModel(draft.selectedModel), selectedModel: draft.selectedModel, imageCount: draft.imageCount, - parameters: {}, + parameters: draft.parameters ?? {}, inputSchema: undefined, useGoogleSearch: false, useImageSearch: false, @@ -959,7 +973,7 @@ function buildInitialDataForNode(nodeType: NodeType, draft: ComposerDraft): Part resolution: draft.resolution, durationSeconds: normalizeVideoDurationSeconds(draft.durationSeconds), soundEnabled: modelSupportsVideoSound(draft.selectedModel) ? draft.soundEnabled ?? true : undefined, - parameters: {}, + parameters: draft.parameters ?? {}, inputSchema: undefined, status: "idle", error: null, @@ -970,7 +984,7 @@ function buildInitialDataForNode(nodeType: NodeType, draft: ComposerDraft): Part inputImages: draft.inputImages, inputPrompt: draft.prompt.trim(), selectedModel: draft.selectedModel, - parameters: {}, + parameters: draft.parameters ?? {}, inputSchema: undefined, status: "idle", error: null, @@ -980,7 +994,7 @@ function buildInitialDataForNode(nodeType: NodeType, draft: ComposerDraft): Part return { inputPrompt: draft.prompt.trim(), selectedModel: draft.selectedModel, - parameters: {}, + parameters: draft.parameters ?? {}, inputSchema: undefined, status: "idle", error: null, @@ -1553,6 +1567,20 @@ export function GenerationComposer() { ? context.node?.data as NanoBananaNodeData | GenerateVideoNodeData | GenerateAudioNodeData | undefined : undefined; const activeParameterSchema = generationNodeData?.parameterSchema ?? []; + useEffect(() => { + if (context.mode !== "node-edit" || !context.node || activeParameterSchema.length === 0) return; + const normalized = applyGatewayDimensionParameterDefaults(activeParameterSchema, draftRef.current.parameters); + if (!normalized.changed) return; + + setDraft((current) => { + const currentNormalized = applyGatewayDimensionParameterDefaults(activeParameterSchema, current.parameters); + if (!currentNormalized.changed) return current; + const next = { ...current, parameters: currentNormalized.parameters }; + draftRef.current = next; + return next; + }); + updateNodeData(context.node.id, { parameters: normalized.parameters }); + }, [activeParameterSchema, context, updateNodeData]); const gatewayDimensionSelects = buildGatewayDimensionSelects(activeParameterSchema, draft); const imageCountOptions = getImageCountOptions(activeParameterSchema); const visibleImageCountOptions = imageCountOptions.length > 0 diff --git a/src/components/nodes/PromptNode.tsx b/src/components/nodes/PromptNode.tsx index fa8e5a7b..f07fef72 100644 --- a/src/components/nodes/PromptNode.tsx +++ b/src/components/nodes/PromptNode.tsx @@ -235,10 +235,14 @@ export function PromptNode({ id, data, selected }: NodeProps) { setIsEditing(true); } }} - className="flex h-full w-full cursor-text items-center justify-center rounded-lg bg-neutral-800 p-8 text-center outline-none" + className={`nodrag nopan nowheel flex h-full w-full cursor-text rounded-lg bg-neutral-800 p-8 outline-none ${ + promptText + ? "items-start justify-start overflow-auto text-left select-text" + : "items-center justify-center text-center" + }`} > {promptText ? ( -
+
{localPrompt}
) : (