diff --git a/src/app/api/popi/media/upload/route.ts b/src/app/api/popi/media/upload/route.ts index dc0ced94..fe6f30c4 100644 --- a/src/app/api/popi/media/upload/route.ts +++ b/src/app/api/popi/media/upload/route.ts @@ -65,6 +65,15 @@ function mediaResourceUrl(media: PopiMedia): string | null { return media.url || media.originUrl || media.path || media.originPath || null; } +function isUploadedMedia(value: PopiMediaUploadData): value is PopiMedia { + return ( + "url" in value || + "originUrl" in value || + "path" in value || + "originPath" in value + ); +} + function uploadedMediaUrls(data: PopiMediaUploadData | undefined): string[] { if (!data) return []; if (Array.isArray(data)) { @@ -81,7 +90,7 @@ function uploadedMediaUrls(data: PopiMediaUploadData | undefined): string[] { if (list) { return list.map(mediaResourceUrl).filter((url): url is string => Boolean(url)); } - const single = mediaResourceUrl(data); + const single = isUploadedMedia(data) ? mediaResourceUrl(data) : null; return single ? [single] : []; } diff --git a/src/components/__tests__/WorkflowCanvas.test.tsx b/src/components/__tests__/WorkflowCanvas.test.tsx index 4486ddb0..668e56dc 100644 --- a/src/components/__tests__/WorkflowCanvas.test.tsx +++ b/src/components/__tests__/WorkflowCanvas.test.tsx @@ -480,7 +480,9 @@ describe("WorkflowCanvas", () => { fireEvent.click(screen.getByText("Create Generate Image")); - expect(mockAddNode).toHaveBeenCalledWith("nanoBanana", { x: 560, y: 100 }); + await waitFor(() => { + expect(mockAddNode).toHaveBeenCalledWith("nanoBanana", { x: 560, y: 100 }, undefined); + }); expect(mockOnConnect).toHaveBeenCalledWith({ source: "image-1", sourceHandle: "image", @@ -778,7 +780,7 @@ describe("WorkflowCanvas", () => { expect(screen.queryByText("Drop to create node")).not.toBeInTheDocument(); }); - it("should call addNode when node type is dropped on canvas", () => { + it("should call addNode when node type is dropped on canvas", async () => { render( @@ -803,7 +805,9 @@ describe("WorkflowCanvas", () => { clientY: 300, }); - expect(mockAddNode).toHaveBeenCalledWith("prompt", expect.any(Object)); + await waitFor(() => { + expect(mockAddNode).toHaveBeenCalledWith("prompt", expect.any(Object), undefined); + }); }); it("should create a prompt node from dropped Markdown files", async () => { @@ -929,7 +933,7 @@ describe("WorkflowCanvas", () => { expect(mockUpdateNodeData).not.toHaveBeenCalled(); }); - it("should add prompt node on Shift+P", () => { + it("should add prompt node on Shift+P", async () => { render( @@ -938,10 +942,12 @@ describe("WorkflowCanvas", () => { fireEvent.keyDown(window, { key: "p", shiftKey: true }); - expect(mockAddNode).toHaveBeenCalledWith("prompt", expect.any(Object)); + await waitFor(() => { + expect(mockAddNode).toHaveBeenCalledWith("prompt", expect.any(Object), undefined); + }); }); - it("should add imageInput node on Shift+I", () => { + it("should add imageInput node on Shift+I", async () => { render( @@ -950,10 +956,12 @@ describe("WorkflowCanvas", () => { fireEvent.keyDown(window, { key: "i", shiftKey: true }); - expect(mockAddNode).toHaveBeenCalledWith("imageInput", expect.any(Object)); + await waitFor(() => { + expect(mockAddNode).toHaveBeenCalledWith("imageInput", expect.any(Object), undefined); + }); }); - it("should add nanoBanana node on Shift+G", () => { + it("should add nanoBanana node on Shift+G", async () => { render( @@ -962,10 +970,12 @@ describe("WorkflowCanvas", () => { fireEvent.keyDown(window, { key: "g", shiftKey: true }); - expect(mockAddNode).toHaveBeenCalledWith("nanoBanana", expect.any(Object)); + await waitFor(() => { + expect(mockAddNode).toHaveBeenCalledWith("nanoBanana", expect.any(Object), undefined); + }); }); - it("should add llmGenerate node on Shift+L", () => { + it("should add llmGenerate node on Shift+L", async () => { render( @@ -974,10 +984,12 @@ describe("WorkflowCanvas", () => { fireEvent.keyDown(window, { key: "l", shiftKey: true }); - expect(mockAddNode).toHaveBeenCalledWith("llmGenerate", expect.any(Object)); + await waitFor(() => { + expect(mockAddNode).toHaveBeenCalledWith("llmGenerate", expect.any(Object), undefined); + }); }); - it("should add annotation node on Shift+A", () => { + it("should add annotation node on Shift+A", async () => { render( @@ -986,7 +998,9 @@ describe("WorkflowCanvas", () => { fireEvent.keyDown(window, { key: "a", shiftKey: true }); - expect(mockAddNode).toHaveBeenCalledWith("annotation", expect.any(Object)); + await waitFor(() => { + expect(mockAddNode).toHaveBeenCalledWith("annotation", expect.any(Object), undefined); + }); }); }); diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index 8af1ff20..1c890d44 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -374,6 +374,13 @@ function findProviderModelForSelectedModel(models: ProviderModel[], selectedMode function selectedModelNeedsPopiserverMetadataLookup(model: SelectedModel): boolean { if (model.provider !== "popiserver") return false; + const hasResolvableIdentifier = Boolean( + model.modelId || + model.metadata?.aiModelId || + model.metadata?.aiModelCode || + model.metadata?.aiModelCodeAlias + ); + if (!hasResolvableIdentifier) return false; const billingMethod = finiteIntegerFromUnknown(model.metadata?.billingMethod); if (billingMethod === null) return true; if (billingMethod !== 1) return false; @@ -1150,9 +1157,11 @@ export function GenerationComposer() { const estimatedReferenceImageCount = context.mode === "node-edit" && (connectedInputs?.images.length ?? 0) > 0 ? connectedInputs?.images.length ?? 0 : draft.inputImages.length; + const selectedModelNeedsSchemaLookup = selectedModelNeedsPopiserverMetadataLookup(draft.selectedModel); const modelSchemaReadyForEstimate = context.mode !== "node-edit" || !context.node || + !selectedModelNeedsSchemaLookup || Array.isArray((context.node.data as { parameterSchema?: ModelParameter[] }).parameterSchema); const seedanceEstimatePayload = useMemo(() => { @@ -1211,6 +1220,7 @@ export function GenerationComposer() { const resolvedModel = selectedModelFromProvider(matchedModel); setDraft((current) => { + const currentCapabilities = current.selectedModel.capabilities ?? []; if ( !isSameSelectedModel(current.selectedModel, selectedModel) || !selectedModelNeedsPopiserverMetadataLookup(current.selectedModel) @@ -1222,8 +1232,8 @@ export function GenerationComposer() { ...current, selectedModel: { ...resolvedModel, - capabilities: current.selectedModel.capabilities.length > 0 - ? current.selectedModel.capabilities + capabilities: currentCapabilities.length > 0 + ? currentCapabilities : resolvedModel.capabilities, }, }; @@ -1242,10 +1252,11 @@ export function GenerationComposer() { nodeModel && selectedModelNeedsPopiserverMetadataLookup(nodeModel) ) { + const nodeModelCapabilities = nodeModel.capabilities ?? []; updateNodeData(activeContext.node.id, { selectedModel: { ...resolvedModel, - capabilities: nodeModel.capabilities.length > 0 ? nodeModel.capabilities : resolvedModel.capabilities, + capabilities: nodeModelCapabilities.length > 0 ? nodeModelCapabilities : resolvedModel.capabilities, }, }); } diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index 0e899aee..0a42a196 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -511,9 +511,14 @@ export function GenerateImageNode({ id, data, selected }: NodeProps model.id === nodeData.selectedModel?.modelId); + const shouldFetchPrimaryModelSchema = + Boolean(nodeData.selectedModel?.modelId) && + !usesFirstClassImageControls && + canFetchPopiModelSchema && + (currentProvider === POPI_PROVIDER_ID || showInlineNodeSettings || Boolean(nodeData.modelSchemaRequestId)); const modelSchema = useSelectedModelSchema({ selectedModel: nodeData.selectedModel, - enabled: Boolean(nodeData.selectedModel?.modelId) && !usesFirstClassImageControls && canFetchPopiModelSchema, + enabled: shouldFetchPrimaryModelSchema, refreshKey: nodeData.modelSchemaRequestId, onLoaded: ({ inputs, parameters, model }) => { const schemaDefaults = buildImageDefaultsFromSchema(parameters); diff --git a/src/utils/modelSchemaDefaults.ts b/src/utils/modelSchemaDefaults.ts index e8c01392..dfd6fc55 100644 --- a/src/utils/modelSchemaDefaults.ts +++ b/src/utils/modelSchemaDefaults.ts @@ -38,7 +38,7 @@ export function buildImageDefaultsFromSchema(schema: ModelParameter[]): { const imageCount = findParameter(schema, IMAGE_COUNT_PARAMETER_NAMES); const ratioValue = ratio ? getParameterDefault(ratio) : undefined; const resolutionValue = resolution ? getParameterDefault(resolution) : undefined; - const imageCountValue = imageCount ? Number(getParameterDefault(imageCount)) : undefined; + const imageCountValue = imageCount ? Number(getParameterDefault(imageCount)) : NaN; return { ...(hasValue(ratioValue) ? { aspectRatio: String(ratioValue) as AspectRatio } : {}),