diff --git a/src/components/CostIndicator.tsx b/src/components/CostIndicator.tsx
index e07e872b..e701d206 100644
--- a/src/components/CostIndicator.tsx
+++ b/src/components/CostIndicator.tsx
@@ -2,7 +2,7 @@
import { useState, useMemo } from "react";
import { useWorkflowStore } from "@/store/workflowStore";
-import { calculatePredictedCost, formatCost } from "@/utils/costCalculator";
+import { calculatePredictedCost, formatCost, hasNonGeminiProviders } from "@/utils/costCalculator";
import { CostDialog } from "./CostDialog";
export function CostIndicator() {
@@ -14,9 +14,10 @@ export function CostIndicator() {
return calculatePredictedCost(nodes);
}, [nodes]);
+ const nonGemini = useMemo(() => hasNonGeminiProviders(nodes), [nodes]);
const hasAnyNodes = predictedCost.nodeCount > 0;
- if (!hasAnyNodes && incurredCost === 0) {
+ if (nonGemini || (!hasAnyNodes && incurredCost === 0)) {
return null;
}
diff --git a/src/components/__tests__/CostIndicator.test.tsx b/src/components/__tests__/CostIndicator.test.tsx
index c1fc165c..84350c43 100644
--- a/src/components/__tests__/CostIndicator.test.tsx
+++ b/src/components/__tests__/CostIndicator.test.tsx
@@ -441,4 +441,170 @@ describe("CostIndicator", () => {
expect(screen.queryByTitle("View cost details")).not.toBeInTheDocument();
});
});
+
+ describe("Non-Gemini Provider Hiding", () => {
+ it("should not render when a nanoBanana node has a non-Gemini selectedModel", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "node-1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ selectedModel: {
+ provider: "fal",
+ modelId: "fal-ai/flux",
+ displayName: "Flux",
+ },
+ },
+ },
+ ];
+
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ return selector(createDefaultState({ nodes }));
+ });
+
+ render();
+
+ expect(screen.queryByTitle("View cost details")).not.toBeInTheDocument();
+ });
+
+ it("should not render when a generateVideo node has a non-Gemini selectedModel", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "node-1",
+ type: "generateVideo",
+ position: { x: 0, y: 0 },
+ data: {
+ selectedModel: {
+ provider: "kie",
+ modelId: "kling-video",
+ displayName: "Kling Video",
+ },
+ status: "idle",
+ },
+ },
+ ];
+
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ return selector(createDefaultState({ nodes }));
+ });
+
+ render();
+
+ expect(screen.queryByTitle("View cost details")).not.toBeInTheDocument();
+ });
+
+ it("should not render when a generate3d node has a non-Gemini selectedModel", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "node-1",
+ type: "generate3d",
+ position: { x: 0, y: 0 },
+ data: {
+ selectedModel: {
+ provider: "fal",
+ modelId: "fal-3d",
+ displayName: "Fal 3D",
+ },
+ status: "idle",
+ },
+ },
+ ];
+
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ return selector(createDefaultState({ nodes }));
+ });
+
+ render();
+
+ expect(screen.queryByTitle("View cost details")).not.toBeInTheDocument();
+ });
+
+ it("should render when a nanoBanana node has selectedModel with provider gemini", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "node-1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ selectedModel: {
+ provider: "gemini",
+ modelId: "nano-banana",
+ displayName: "Nano Banana",
+ },
+ },
+ },
+ ];
+
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ return selector(createDefaultState({ nodes }));
+ });
+
+ render();
+
+ expect(screen.getByTitle("View cost details")).toBeInTheDocument();
+ });
+
+ it("should render when a nanoBanana node has no selectedModel (legacy Gemini)", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "node-1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ },
+ },
+ ];
+
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ return selector(createDefaultState({ nodes }));
+ });
+
+ render();
+
+ expect(screen.getByTitle("View cost details")).toBeInTheDocument();
+ });
+
+ it("should not render when mix of Gemini and non-Gemini nodes exist", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "node-1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ },
+ },
+ {
+ id: "node-2",
+ type: "nanoBanana",
+ position: { x: 100, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ selectedModel: {
+ provider: "replicate",
+ modelId: "some-model",
+ displayName: "Some Model",
+ },
+ },
+ },
+ ];
+
+ mockUseWorkflowStore.mockImplementation((selector) => {
+ return selector(createDefaultState({ nodes }));
+ });
+
+ render();
+
+ expect(screen.queryByTitle("View cost details")).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/src/utils/__tests__/costCalculator.test.ts b/src/utils/__tests__/costCalculator.test.ts
new file mode 100644
index 00000000..e94d1b19
--- /dev/null
+++ b/src/utils/__tests__/costCalculator.test.ts
@@ -0,0 +1,235 @@
+import { describe, it, expect } from "vitest";
+import { hasNonGeminiProviders } from "@/utils/costCalculator";
+import { WorkflowNode } from "@/types";
+
+describe("hasNonGeminiProviders", () => {
+ it("should return false for empty nodes array", () => {
+ expect(hasNonGeminiProviders([])).toBe(false);
+ });
+
+ it("should return false for non-generation nodes only", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "prompt",
+ position: { x: 0, y: 0 },
+ data: { prompt: "test" },
+ },
+ {
+ id: "2",
+ type: "imageInput",
+ position: { x: 0, y: 0 },
+ data: {},
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(false);
+ });
+
+ it("should return false for nanoBanana node with no selectedModel (legacy Gemini)", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: { model: "nano-banana", resolution: "1K" },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(false);
+ });
+
+ it("should return false for nanoBanana node with gemini selectedModel", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana-pro",
+ resolution: "1K",
+ selectedModel: {
+ provider: "gemini",
+ modelId: "nano-banana-pro",
+ displayName: "Nano Banana Pro",
+ },
+ },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(false);
+ });
+
+ it("should return true for nanoBanana node with fal provider", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ selectedModel: {
+ provider: "fal",
+ modelId: "fal-ai/flux",
+ displayName: "Flux",
+ },
+ },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(true);
+ });
+
+ it("should return true for nanoBanana node with replicate provider", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ selectedModel: {
+ provider: "replicate",
+ modelId: "some-model",
+ displayName: "Some Model",
+ },
+ },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(true);
+ });
+
+ it("should return true for nanoBanana node with kie provider", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ selectedModel: {
+ provider: "kie",
+ modelId: "kie-model",
+ displayName: "Kie Model",
+ },
+ },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(true);
+ });
+
+ it("should return true for nanoBanana node with wavespeed provider", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ selectedModel: {
+ provider: "wavespeed",
+ modelId: "ws-model",
+ displayName: "WaveSpeed Model",
+ },
+ },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(true);
+ });
+
+ it("should return true for generateVideo node with non-Gemini provider", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "generateVideo",
+ position: { x: 0, y: 0 },
+ data: {
+ selectedModel: {
+ provider: "kie",
+ modelId: "kling-video",
+ displayName: "Kling Video",
+ },
+ status: "idle",
+ },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(true);
+ });
+
+ it("should return false for generateVideo node with no selectedModel", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "generateVideo",
+ position: { x: 0, y: 0 },
+ data: { status: "idle" },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(false);
+ });
+
+ it("should return true for generate3d node with non-Gemini provider", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "generate3d",
+ position: { x: 0, y: 0 },
+ data: {
+ selectedModel: {
+ provider: "fal",
+ modelId: "fal-3d",
+ displayName: "Fal 3D",
+ },
+ status: "idle",
+ },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(true);
+ });
+
+ it("should return false for generate3d node with no selectedModel", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "generate3d",
+ position: { x: 0, y: 0 },
+ data: { status: "idle" },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(false);
+ });
+
+ it("should return true when mixed Gemini and non-Gemini nodes exist", () => {
+ const nodes: WorkflowNode[] = [
+ {
+ id: "1",
+ type: "nanoBanana",
+ position: { x: 0, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ selectedModel: {
+ provider: "gemini",
+ modelId: "nano-banana",
+ displayName: "Nano Banana",
+ },
+ },
+ },
+ {
+ id: "2",
+ type: "nanoBanana",
+ position: { x: 100, y: 0 },
+ data: {
+ model: "nano-banana",
+ resolution: "1K",
+ selectedModel: {
+ provider: "fal",
+ modelId: "fal-ai/flux",
+ displayName: "Flux",
+ },
+ },
+ },
+ ];
+ expect(hasNonGeminiProviders(nodes)).toBe(true);
+ });
+});
diff --git a/src/utils/costCalculator.ts b/src/utils/costCalculator.ts
index 8f59cea8..320faf21 100644
--- a/src/utils/costCalculator.ts
+++ b/src/utils/costCalculator.ts
@@ -1,4 +1,4 @@
-import { ModelType, Resolution, NanoBananaNodeData, GenerateVideoNodeData, SplitGridNodeData, WorkflowNode, ProviderType } from "@/types";
+import { ModelType, Resolution, NanoBananaNodeData, GenerateVideoNodeData, Generate3DNodeData, SplitGridNodeData, WorkflowNode, ProviderType } from "@/types";
// Pricing in USD per image (Gemini API)
export const PRICING = {
@@ -240,6 +240,28 @@ export function calculatePredictedCost(
};
}
+/**
+ * Check whether any generation node in the workflow uses a non-Gemini provider.
+ * Used to hide the CostIndicator when pricing data would be incomplete/misleading.
+ */
+export function hasNonGeminiProviders(nodes: WorkflowNode[]): boolean {
+ return nodes.some((node) => {
+ if (node.type === "nanoBanana") {
+ const data = node.data as NanoBananaNodeData;
+ return data.selectedModel?.provider !== undefined && data.selectedModel.provider !== "gemini";
+ }
+ if (node.type === "generateVideo") {
+ const data = node.data as GenerateVideoNodeData;
+ return data.selectedModel?.provider !== undefined && data.selectedModel.provider !== "gemini";
+ }
+ if (node.type === "generate3d") {
+ const data = node.data as Generate3DNodeData;
+ return data.selectedModel?.provider !== undefined && data.selectedModel.provider !== "gemini";
+ }
+ return false;
+ });
+}
+
export function formatCost(cost: number): string {
if (cost === 0) return "$0.00";
if (cost < 0.01) return "<$0.01";