From 065af2c375f496163a923864448a008a91ca643c Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 16 Jan 2026 20:55:43 +1300 Subject: [PATCH] feat(24-03): restructure CostDialog for provider-grouped breakdown - Redesign CostDialog to show provider-grouped cost information - Add two-section layout: Known Costs (Gemini, fal.ai) and Pricing Unavailable (Replicate) - Add ProviderIcon component with colored dots for each provider - Group breakdown items by provider with provider headers - Show billing units (per image, per video, per second) for each model - Add Replicate help text with link to replicate.com - Update Incurred Cost section with note "Tracks Gemini and fal.ai only" - Hide empty sections dynamically Co-Authored-By: Claude Opus 4.5 --- src/components/CostDialog.tsx | 233 ++++++++++++++++++++++++++++++---- 1 file changed, 211 insertions(+), 22 deletions(-) diff --git a/src/components/CostDialog.tsx b/src/components/CostDialog.tsx index db7b992c..ff5a1dbd 100644 --- a/src/components/CostDialog.tsx +++ b/src/components/CostDialog.tsx @@ -2,7 +2,8 @@ import { useEffect } from "react"; import { useWorkflowStore } from "@/store/workflowStore"; -import { PredictedCostResult, formatCost, PRICING } from "@/utils/costCalculator"; +import { PredictedCostResult, CostBreakdownItem, formatCost } from "@/utils/costCalculator"; +import { ProviderType } from "@/types/providers"; interface CostDialogProps { predictedCost: PredictedCostResult; @@ -10,6 +11,72 @@ interface CostDialogProps { onClose: () => void; } +/** + * Provider icon component - colored dot with provider indicator + */ +function ProviderIcon({ provider }: { provider: ProviderType }) { + const colors: Record = { + gemini: { bg: "bg-green-500/20", text: "text-green-300" }, + fal: { bg: "bg-purple-500/20", text: "text-purple-300" }, + replicate: { bg: "bg-blue-500/20", text: "text-blue-300" }, + openai: { bg: "bg-teal-500/20", text: "text-teal-300" }, + }; + + const labels: Record = { + gemini: "G", + fal: "f", + replicate: "R", + openai: "O", + }; + + const color = colors[provider] || colors.gemini; + + return ( + + {labels[provider]} + + ); +} + +/** + * Format unit for display (e.g., "image" -> "per image", "second" -> "per second") + */ +function formatUnit(unit: string): string { + if (unit === "image") return "per image"; + if (unit === "video") return "per video"; + if (unit === "second") return "per second"; + return `per ${unit}`; +} + +/** + * Get display name for provider + */ +function getProviderDisplayName(provider: ProviderType): string { + const names: Record = { + gemini: "Gemini", + fal: "fal.ai", + replicate: "Replicate", + openai: "OpenAI", + }; + return names[provider] || provider; +} + +/** + * Group breakdown items by provider + */ +function groupByProvider(breakdown: CostBreakdownItem[]): Map { + const grouped = new Map(); + breakdown.forEach((item) => { + const existing = grouped.get(item.provider); + if (existing) { + existing.push(item); + } else { + grouped.set(item.provider, [item]); + } + }); + return grouped; +} + export function CostDialog({ predictedCost, incurredCost, onClose }: CostDialogProps) { const resetIncurredCost = useWorkflowStore((state) => state.resetIncurredCost); @@ -29,6 +96,58 @@ export function CostDialog({ predictedCost, incurredCost, onClose }: CostDialogP } }; + // Separate breakdown items into known pricing (Gemini, fal.ai) and unknown pricing (Replicate) + const groupedBreakdown = groupByProvider(predictedCost.breakdown); + + // Known pricing providers: Gemini and fal.ai + const knownPricingProviders: ProviderType[] = ["gemini", "fal"]; + const unknownPricingProviders: ProviderType[] = ["replicate"]; + + const knownCostItems: CostBreakdownItem[] = []; + const unknownCostItems: CostBreakdownItem[] = []; + + knownPricingProviders.forEach((provider) => { + const items = groupedBreakdown.get(provider); + if (items) { + knownCostItems.push(...items); + } + }); + + unknownPricingProviders.forEach((provider) => { + const items = groupedBreakdown.get(provider); + if (items) { + unknownCostItems.push(...items); + } + }); + + // Calculate known costs total (only from Gemini and fal.ai) + const knownCostsTotal = knownCostItems.reduce((sum, item) => sum + (item.subtotal ?? 0), 0); + + // Group known items by provider for display + const knownByProvider = new Map(); + knownCostItems.forEach((item) => { + const existing = knownByProvider.get(item.provider); + if (existing) { + existing.push(item); + } else { + knownByProvider.set(item.provider, [item]); + } + }); + + // Group unknown items by provider for display + const unknownByProvider = new Map(); + unknownCostItems.forEach((item) => { + const existing = unknownByProvider.get(item.provider); + if (existing) { + existing.push(item); + } else { + unknownByProvider.set(item.provider, [item]); + } + }); + + const hasKnownCosts = knownCostItems.length > 0; + const hasUnknownCosts = unknownCostItems.length > 0; + return (
@@ -47,7 +166,7 @@ export function CostDialog({ predictedCost, incurredCost, onClose }: CostDialogP
- {/* Predicted Cost Section */} + {/* Predicted Cost Section - Split into Known and Unknown */}
Predicted Cost @@ -56,26 +175,79 @@ export function CostDialog({ predictedCost, incurredCost, onClose }: CostDialogP
- {predictedCost.breakdown.length > 0 && ( -
- {predictedCost.breakdown.map((item, idx) => ( -
- - {item.count}x {item.modelName} - {item.unitCost === null && " (no pricing)"} - - - {item.subtotal !== null ? formatCost(item.subtotal) : "—"} - -
- ))} + {/* Known Costs Section */} + {hasKnownCosts && ( +
+
+ Known Costs + + {formatCost(knownCostsTotal)} + +
+ +
+ {Array.from(knownByProvider.entries()).map(([provider, items]) => ( +
+
+ + {getProviderDisplayName(provider)} +
+ {items.map((item, idx) => ( +
+ + {item.count}x {item.modelName} + ({formatUnit(item.unit)}) + + + {item.subtotal !== null ? formatCost(item.subtotal) : "—"} + +
+ ))} +
+ ))} +
)} - {predictedCost.unknownPricingCount > 0 && ( -

- {predictedCost.unknownPricingCount} model{predictedCost.unknownPricingCount > 1 ? "s" : ""} without pricing -

+ {/* Pricing Unavailable Section */} + {hasUnknownCosts && ( +
+
+ Pricing Unavailable +
+ +
+ {Array.from(unknownByProvider.entries()).map(([provider, items]) => ( +
+
+ + {getProviderDisplayName(provider)} +
+ {items.map((item, idx) => ( +
+ + {item.count}x {item.modelName} + + +
+ ))} +
+ ))} +
+ +

+ Replicate pricing varies by hardware and runtime. Check{" "} + + replicate.com + {" "} + for details. +

+
)} {predictedCost.nodeCount === 0 && ( @@ -96,6 +268,9 @@ export function CostDialog({ predictedCost, incurredCost, onClose }: CostDialogP

Actual API spend from successful generations

+

+ Tracks Gemini and fal.ai only +

{incurredCost > 0 && (