Compare commits

...
Author SHA1 Message Date
Brandon Thomas 8052396ca6 cleanup 2026-03-02 06:03:56 -05:00
Brandon Thomas 538192110c get rid of logging 2026-03-02 05:57:04 -05:00
Brandon Thomas d48ee450b8 cost estimating ux 2026-03-02 05:16:51 -05:00
Brandon Thomas 374590a918 NBP pricing on text to image works 2026-03-02 05:16:51 -05:00
21 changed files with 405 additions and 28 deletions
@@ -971,7 +971,7 @@ const PageDraw = () => {
/>
</div>
<div className="absolute bottom-6 right-6 z-20 flex items-center gap-2">
<CostCalculatorButton />
<CostCalculatorButton modelPage={PAGE_ID} />
<HelpMenuButton />
</div>
</>
@@ -172,7 +172,7 @@ export const BaseImageSelector = ({
</div>
</div>
<div className="fixed bottom-6 right-6 z-20 flex items-center gap-2">
<CostCalculatorButton />
<CostCalculatorButton modelPage={ModelPage.ImageEditor} />
<HelpMenuButton />
</div>
<GalleryModal
@@ -32,7 +32,10 @@ import {
import { CommonAspectRatio, ImageModel } from "@storyteller/model-list";
import { HistoryStack, ImageBundle } from "./HistoryStack";
import { HelpMenuButton } from "@storyteller/ui-help-menu";
import { CostCalculatorButton } from "@storyteller/ui-pricing-modal";
import {
CostCalculatorButton,
useCostBreakdownModalStore,
} from "@storyteller/ui-pricing-modal";
import { GenerationProvider } from "@storyteller/api-enums";
const PAGE_ID: ModelPage = ModelPage.ImageEditor;
@@ -46,6 +49,10 @@ const PageEdit = () => {
const selectedProvider: GenerationProvider | undefined =
useSelectedProviderForModel(PAGE_ID, selectedImageModel?.id);
const imageCredits = useCostBreakdownModalStore(
(s) => s.estimatedCreditsByPage[PAGE_ID],
);
// State for canvas dimensions
const canvasWidth = useRef<number>(1024);
const canvasHeight = useRef<number>(1024);
@@ -663,6 +670,7 @@ const PageEdit = () => {
isNanoBananaModel={isNanoBananaModel}
onUndo={store.undo}
onRedo={store.redo}
credits={imageCredits}
/>
</div>
<div className="relative z-0">
@@ -734,7 +742,7 @@ const PageEdit = () => {
/>
</div>
<div className="absolute bottom-6 right-6 z-20 flex items-center gap-2">
<CostCalculatorButton />
<CostCalculatorButton modelPage={PAGE_ID} />
<HelpMenuButton />
</div>
</>
@@ -88,7 +88,10 @@ import { useTabStore } from "../Stores/TabState";
import PageEdit from "../PageEdit/PageEdit";
import { ImageModel } from "@storyteller/model-list";
import { HelpMenuButton } from "@storyteller/ui-help-menu";
import { CostCalculatorButton } from "@storyteller/ui-pricing-modal";
import {
CostCalculatorButton,
useCostBreakdownModalStore,
} from "@storyteller/ui-pricing-modal";
import { GenerationProvider } from "@storyteller/api-enums";
const PAGE_ID: ModelPage = ModelPage.Stage3D;
@@ -123,6 +126,10 @@ export const PageEditor = () => {
const selectedProvider: GenerationProvider | undefined =
useSelectedProviderForModel(PAGE_ID, selectedImageModel?.id);
const imageCredits = useCostBreakdownModalStore(
(s) => s.estimatedCreditsByPage[PAGE_ID],
);
const height =
dndTimelineHeight.value > -1
? pageHeight.value - dndTimelineHeight.value - 56
@@ -537,6 +544,7 @@ export const PageEditor = () => {
onAspectRatioSelect={onAspectRatioSelect}
selectedImageModel={selectedImageModel}
selectedProvider={selectedProvider}
credits={imageCredits}
setEnginePrompt={(prompt) => {
console.log("setEnginePrompt", prompt);
if (!editorEngine) {
@@ -569,7 +577,7 @@ export const PageEditor = () => {
/>
</div>
<div className="absolute bottom-6 right-6 z-20 flex items-center gap-2">
<CostCalculatorButton />
<CostCalculatorButton modelPage={PAGE_ID} />
<HelpMenuButton />
</div>
</div>
@@ -28,7 +28,10 @@ import {
import { Badge } from "@storyteller/ui-badge";
import { twMerge } from "tailwind-merge";
import { HelpMenuButton } from "@storyteller/ui-help-menu";
import { CostCalculatorButton } from "@storyteller/ui-pricing-modal";
import {
CostCalculatorButton,
useCostBreakdownModalStore,
} from "@storyteller/ui-pricing-modal";
import { GenerationProvider } from "@storyteller/api-enums";
const PAGE_ID: ModelPage = ModelPage.TextToImage;
@@ -49,6 +52,10 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
const selectedProvider: GenerationProvider | undefined =
useSelectedProviderForModel(PAGE_ID, selectedImageModel?.id);
const imageCredits = useCostBreakdownModalStore(
(s) => s.estimatedCreditsByPage[PAGE_ID],
);
const jobContext: JobContextType = {
jobTokens: [],
addJobToken: () => {},
@@ -217,6 +224,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
imageMediaId={imageMediaId}
url={imageUrl ?? undefined}
onImageRowVisibilityChange={setImageRowVisible}
credits={imageCredits}
onEnqueuePressed={async (prompt, count, subscriberId) => {
const modelLabel = selectedImageModel?.fullName ?? "";
startBatch(prompt, count, modelLabel, subscriberId);
@@ -240,7 +248,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
/>
</div>
<div className="absolute bottom-6 right-6 z-20 flex items-center gap-2">
<CostCalculatorButton />
<CostCalculatorButton modelPage={PAGE_ID} />
<HelpMenuButton />
</div>
</div>
@@ -0,0 +1,7 @@
// NOTE: These are defined in Rust (as the source of truth) and duplicated in the frontend.
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
export enum CommonImageModel {
NanaBananaPro = "nano_banana_pro",
}
@@ -0,0 +1,7 @@
// NOTE: These are defined in Rust (as the source of truth) and duplicated in the frontend.
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
export type ImageGenerationMode =
| { type: "text_to_image" }
| { type: "image_edit"; count: number };
@@ -1,4 +1,6 @@
export * from "./CommonImageModel.js";
export * from "./CommonVideoModel.js";
export * from "./CommonAspectRatio.js";
export * from "./CommonVideoResolution.js";
export * from "./GenerationMode.js";
export * from "./ImageGenerationMode.js";
@@ -0,0 +1,34 @@
import { CommonAspectRatio } from "@storyteller/api-enums";
// TODO(bt): This shouldn't exist. We need to standardize types throughout the frontend.
const VALID_COMMON_ASPECT_RATIOS = new Set<string>(
Object.values(CommonAspectRatio),
);
/**
* Convert an aspect ratio to a CommonAspectRatio for the image cost estimate API.
*
* Accepts either:
* - A new-style CommonAspectRatio value (e.g. "wide_sixteen_by_nine") — used when the model
* supports the new aspect ratio picker and the value is stored/passed as a string.
* - A legacy aspect ratio string ("wide" | "tall" | "square" | "auto") — used by the old
* prompt stores that predate the new CommonAspectRatio enum.
*/
export function imageAspectRatioToCommonAspectRatio(
newStyleAspectRatio: string | undefined,
legacyAspectRatio?: string,
): CommonAspectRatio | null {
if (newStyleAspectRatio && VALID_COMMON_ASPECT_RATIOS.has(newStyleAspectRatio)) {
return newStyleAspectRatio as CommonAspectRatio;
}
if (legacyAspectRatio) {
switch (legacyAspectRatio) {
case "wide": return CommonAspectRatio.Wide;
case "tall": return CommonAspectRatio.Tall;
case "square": return CommonAspectRatio.Square;
case "auto": return CommonAspectRatio.Auto;
}
}
return null;
}
@@ -0,0 +1,14 @@
import { CommonImageModel } from "@storyteller/api-enums";
// TODO(bt): This shouldn't exist. We need to standardize types throughout the frontend.
export function imageModelToCommonImageModel(
tauriId: string,
): CommonImageModel | null {
switch (tauriId) {
case "nano_banana_pro":
return CommonImageModel.NanaBananaPro;
default:
return null;
}
}
@@ -0,0 +1,29 @@
import { CommonVideoResolution } from "@storyteller/api-enums";
// TODO(bt): This shouldn't exist. We need to standardize types throughout the frontend.
/**
* Convert a resolution value to CommonVideoResolution for the image cost estimate API.
* The image cost API reuses CommonVideoResolution since the values (1k/2k/4k) are the same.
*
* Accepts either:
* - A new-style CommonResolution value from @storyteller/model-list ("one_k", "two_k", "four_k")
* - A legacy resolution string ("1k", "2k", "4k") from the old prompt stores
*/
export function imageResolutionToCommonVideoResolution(
resolution: string | undefined,
): CommonVideoResolution | null {
switch (resolution) {
case "one_k":
case "1k":
return CommonVideoResolution.OneK;
case "two_k":
case "2k":
return CommonVideoResolution.TwoK;
case "four_k":
case "4k":
return CommonVideoResolution.FourK;
default:
return null;
}
}
@@ -0,0 +1,11 @@
import { ImageGenerationMode } from "@storyteller/api-enums";
import { RefImage } from "@storyteller/ui-promptbox";
export function imageStoreToGenerationMode(
referenceImages: RefImage[],
): ImageGenerationMode {
if (referenceImages.length > 0) {
return { type: "image_edit", count: referenceImages.length };
}
return { type: "text_to_image" };
}
@@ -1,3 +1,7 @@
export * from "./videoModelToCommonVideoModel.js";
export * from "./videoAspectRatioToCommonAspectRatio.js";
export * from "./videoStoreToGenerationMode.js";
export * from "./imageModelToCommonImageModel.js";
export * from "./imageAspectRatioToCommonAspectRatio.js";
export * from "./imageResolutionToCommonVideoResolution.js";
export * from "./imageStoreToGenerationMode.js";
@@ -28,6 +28,7 @@ import {
import { Model } from "@storyteller/model-list";
import { useCurrency } from "./use-currency";
import { useVideoCostEstimate } from "./useVideoCostEstimate";
import { useImageCostEstimate } from "./useImageCostEstimate";
// Drag handle subcomponent that Modal looks for
const DragHandle = ({ children }: { children: React.ReactNode }) => (
@@ -113,6 +114,12 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) {
selectedModel,
selectedProvider,
);
const { isLoading: isImageEstimateLoading } = useImageCostEstimate(
activePage,
selectedModel,
selectedProvider,
);
const isEstimateLoading = isVideoEstimateLoading || isImageEstimateLoading;
// Get generation settings from the appropriate stores based on active page
const prompt2D = usePrompt2DStore();
@@ -165,17 +172,24 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) {
const storeData = getStoreData();
// For video, use the live estimate from the backend; for others use a default
const videoCredits =
activePage === ModelPage.ImageToVideo
? (estimatedCreditsByPage[ModelPage.ImageToVideo] ?? null)
: null;
// Pages that use a live backend estimate instead of a local calculation
const LIVE_ESTIMATE_PAGES = new Set<ModelPage>([
ModelPage.TextToImage,
ModelPage.Canvas2D,
ModelPage.Stage3D,
ModelPage.ImageEditor,
ModelPage.ImageToVideo,
]);
const isLiveEstimatePage = LIVE_ESTIMATE_PAGES.has(activePage);
const liveCredits = isLiveEstimatePage
? (estimatedCreditsByPage[activePage] ?? null)
: null;
const creditsPerGeneration = 1;
const totalCredits =
activePage === ModelPage.ImageToVideo
? (videoCredits ?? null)
: creditsPerGeneration * storeData.generationCount;
const totalCredits = isLiveEstimatePage
? liveCredits
: creditsPerGeneration * storeData.generationCount;
// Convert credits to USD first (1 credit = $0.01), then to selected currency
const usdAmount = (totalCredits ?? 0) * 0.01;
@@ -308,8 +322,7 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) {
Credits
</div>
<div className="text-lg font-bold text-base-fg flex items-center gap-1.5">
{isVideoEstimateLoading &&
activePage === ModelPage.ImageToVideo ? (
{isEstimateLoading && isLiveEstimatePage ? (
<>
<FontAwesomeIcon
icon={faSpinner}
@@ -0,0 +1,156 @@
import { useEffect, useState } from "react";
import { ModelPage } from "@storyteller/ui-model-selector";
import { Model } from "@storyteller/model-list";
import { GenerationProvider } from "@storyteller/api-enums";
import {
usePromptImageStore,
usePrompt2DStore,
usePrompt3DStore,
usePromptEditStore,
} from "@storyteller/ui-promptbox";
import {
EstimateImageCost,
isEstimateImageCostSuccess,
} from "@storyteller/tauri-api";
import { useCostBreakdownModalStore } from "./cost-breakdown-modal-store";
import {
imageModelToCommonImageModel,
imageAspectRatioToCommonAspectRatio,
imageResolutionToCommonVideoResolution,
} from "./convert/index.js";
const IMAGE_PAGES = new Set<ModelPage>([
ModelPage.TextToImage,
ModelPage.Canvas2D,
ModelPage.Stage3D,
ModelPage.ImageEditor,
]);
export function useImageCostEstimate(
activePage: ModelPage,
selectedModel: Model | null | undefined,
selectedProvider: string | null | undefined,
): { isLoading: boolean } {
const [isLoading, setIsLoading] = useState(false);
const setEstimatedCreditsForPage = useCostBreakdownModalStore(
(s) => s.setEstimatedCreditsForPage,
);
// TextToImage store
const imageAspectRatio = usePromptImageStore((s) => s.commonAspectRatio);
const imageLegacyAspectRatio = usePromptImageStore((s) => s.aspectRatio);
const imageResolution = usePromptImageStore((s) => s.commonResolution);
const imageLegacyResolution = usePromptImageStore((s) => s.resolution);
const imageReferenceImages = usePromptImageStore((s) => s.referenceImages);
// Canvas2D store
const prompt2DAspectRatio = usePrompt2DStore((s) => s.aspectRatio);
const prompt2DResolution = usePrompt2DStore((s) => s.resolution);
const prompt2DReferenceImages = usePrompt2DStore((s) => s.referenceImages);
// Stage3D store
const prompt3DResolution = usePrompt3DStore((s) => s.resolution);
const prompt3DReferenceImages = usePrompt3DStore((s) => s.referenceImages);
// ImageEditor store
const editAspectRatio = usePromptEditStore((s) => s.aspectRatio);
const editResolution = usePromptEditStore((s) => s.resolution);
const editReferenceImages = usePromptEditStore((s) => s.referenceImages);
useEffect(() => {
if (!IMAGE_PAGES.has(activePage) || !selectedModel) {
return;
}
const commonModel = imageModelToCommonImageModel(selectedModel.tauriId);
if (!commonModel) {
setEstimatedCreditsForPage(activePage, null);
return;
}
let aspectRatioStr: string | undefined;
let legacyAspectRatioStr: string | undefined;
let resolutionStr: string | undefined;
let referenceImageCount = 0;
switch (activePage) {
case ModelPage.TextToImage:
aspectRatioStr = imageAspectRatio;
legacyAspectRatioStr = imageLegacyAspectRatio;
resolutionStr = imageResolution ?? imageLegacyResolution;
referenceImageCount = imageReferenceImages.length;
break;
case ModelPage.Canvas2D:
legacyAspectRatioStr = prompt2DAspectRatio;
resolutionStr = prompt2DResolution;
referenceImageCount = prompt2DReferenceImages.length;
break;
case ModelPage.Stage3D:
resolutionStr = prompt3DResolution;
referenceImageCount = prompt3DReferenceImages.length;
break;
case ModelPage.ImageEditor:
legacyAspectRatioStr = editAspectRatio;
resolutionStr = editResolution;
referenceImageCount = editReferenceImages.length;
break;
}
const commonAspectRatio = imageAspectRatioToCommonAspectRatio(
aspectRatioStr,
legacyAspectRatioStr,
);
const commonResolution = imageResolutionToCommonVideoResolution(resolutionStr);
const generationMode =
referenceImageCount > 0
? { type: "image_edit" as const, count: referenceImageCount }
: { type: "text_to_image" as const };
const provider =
(selectedProvider as GenerationProvider | null | undefined) ??
GenerationProvider.Artcraft;
setIsLoading(true);
EstimateImageCost({
model: commonModel,
provider,
generation_mode: generationMode,
aspect_ratio: commonAspectRatio ?? undefined,
resolution: commonResolution ?? undefined,
})
.then((result) => {
if (isEstimateImageCostSuccess(result)) {
const credits = result.payload.cost_in_credits ?? null;
setEstimatedCreditsForPage(activePage, credits);
} else {
setEstimatedCreditsForPage(activePage, null);
}
})
.catch(() => {
setEstimatedCreditsForPage(activePage, null);
})
.finally(() => {
setIsLoading(false);
});
}, [
activePage,
selectedModel?.id,
selectedProvider,
imageAspectRatio,
imageLegacyAspectRatio,
imageResolution,
imageLegacyResolution,
imageReferenceImages.length,
prompt2DAspectRatio,
prompt2DResolution,
prompt2DReferenceImages.length,
prompt3DResolution,
prompt3DReferenceImages.length,
editAspectRatio,
editResolution,
editReferenceImages.length,
]);
return { isLoading };
}
@@ -75,6 +75,7 @@ interface PromptBox3DProps {
file: File;
} | null)
| undefined;
credits?: number | null;
}
export const PromptBox3D = ({
@@ -98,6 +99,7 @@ export const PromptBox3D = ({
selectedImageModel,
selectedProvider,
snapshotCurrentFrame,
credits,
}: PromptBox3DProps) => {
useSignals();
//const fileInputRef = useRef<HTMLInputElement>(null);
@@ -747,6 +749,7 @@ export const PromptBox3D = ({
onClick={handleEnqueue}
disabled={isEnqueueing || !prompt.trim()}
loading={isEnqueueing}
credits={credits}
>
Generate
</GenerateButton>
@@ -57,6 +57,7 @@ export interface PromptBoxEditProps {
isNanoBananaModel?: boolean;
onUndo?: () => void;
onRedo?: () => void;
credits?: number | null;
}
export const PromptBoxEdit = ({
@@ -77,6 +78,7 @@ export const PromptBoxEdit = ({
isNanoBananaModel = false,
onUndo,
onRedo,
credits,
}: PromptBoxEditProps) => {
// Check subscription status to determine if user is on free plan
const hasPaidPlan = useSubscriptionState((s) => s.hasPaidPlan);
@@ -515,6 +517,7 @@ export const PromptBoxEdit = ({
onClick={handleGenerate}
disabled={isDisabled || !prompt.trim()}
loading={isEnqueueing ?? internalEnqueueing}
credits={credits}
>
Generate
</GenerateButton>
@@ -52,6 +52,7 @@ interface PromptBoxImageProps {
imageMediaId?: string;
url?: string;
onImageRowVisibilityChange?: (visible: boolean) => void;
credits?: number | null;
}
export const PromptBoxImage = ({
@@ -63,6 +64,7 @@ export const PromptBoxImage = ({
imageMediaId,
url,
onImageRowVisibilityChange,
credits,
}: PromptBoxImageProps) => {
useSignals();
@@ -110,15 +112,11 @@ export const PromptBoxImage = ({
referenceImages.length > 0 ||
uploadingImages.length > 0;
// New aspect ratio state will begin to phase out old aspect ratio
const [commonAspectRatio, setCommonAspectRatio] = useState<
CommonAspectRatio | undefined
>(undefined);
// New resolution state will begin to phase out old resolution
const [commonResolution, setCommonResolution] = useState<
CommonResolution | undefined
>(undefined);
// New aspect ratio and resolution — stored globally so cost estimates can observe them
const commonAspectRatio = usePromptImageStore((s) => s.commonAspectRatio);
const setCommonAspectRatio = usePromptImageStore((s) => s.setCommonAspectRatio);
const commonResolution = usePromptImageStore((s) => s.commonResolution);
const setCommonResolution = usePromptImageStore((s) => s.setCommonResolution);
useEffect(() => {
onImageRowVisibilityChange?.(isImageRowVisible);
@@ -545,6 +543,7 @@ export const PromptBoxImage = ({
onClick={handleEnqueue}
disabled={!prompt.trim()}
loading={isEnqueueing}
credits={credits}
>
Generate
</GenerateButton>
@@ -1,4 +1,6 @@
import { create } from "zustand";
import { CommonAspectRatio } from "@storyteller/model-list";
import { CommonResolution } from "@storyteller/model-list";
export interface RefImage {
id: string;
@@ -74,12 +76,17 @@ interface PromptImageStore {
useSystemPrompt: boolean;
referenceImages: RefImage[];
generationCount: number;
// New-style aspect ratio and resolution (preferred over legacy fields above)
commonAspectRatio: CommonAspectRatio | undefined;
commonResolution: CommonResolution | undefined;
setPrompt: (prompt: string) => void;
setAspectRatio: (ratio: AspectRatio) => void;
setResolution: (resolution: Resolution) => void;
setUseSystemPrompt: (value: boolean) => void;
setReferenceImages: (images: RefImage[]) => void;
setGenerationCount: (count: number) => void;
setCommonAspectRatio: (ratio: CommonAspectRatio | undefined) => void;
setCommonResolution: (resolution: CommonResolution | undefined) => void;
}
export const usePromptImageStore = create<PromptImageStore>()((set) => ({
@@ -89,12 +96,16 @@ export const usePromptImageStore = create<PromptImageStore>()((set) => ({
useSystemPrompt: true,
referenceImages: [],
generationCount: 1,
commonAspectRatio: undefined,
commonResolution: undefined,
setPrompt: (prompt) => set({ prompt }),
setAspectRatio: (aspectRatio) => set({ aspectRatio }),
setResolution: (resolution) => set({ resolution }),
setUseSystemPrompt: (useSystemPrompt) => set({ useSystemPrompt }),
setReferenceImages: (referenceImages) => set({ referenceImages }),
setGenerationCount: (generationCount) => set({ generationCount }),
setCommonAspectRatio: (commonAspectRatio) => set({ commonAspectRatio }),
setCommonResolution: (commonResolution) => set({ commonResolution }),
}));
// ----- Video Prompt Box Store -----
+1
View File
@@ -41,4 +41,5 @@ export * from "./lib/sora/waitForSoraLogin.js";
export * from "./lib/system/GetAppInfo.js";
export * from "./lib/util/LoadWithoutCors.js";
export * from "./lib/worldlabs/WorldLabsGetCredentialInfo.js";
export * from "./lib/cost_estimate/EstimateImageCost.js";
export * from "./lib/cost_estimate/EstimateVideoCost.js";
@@ -0,0 +1,59 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult, CommandSuccessStatus } from "../common/CommandStatus";
import {
CommonImageModel,
CommonAspectRatio,
CommonVideoResolution,
ImageGenerationMode,
GenerationProvider,
} from "@storyteller/api-enums";
export interface EstimateImageCostRequest {
model: CommonImageModel;
provider: GenerationProvider;
generation_mode: ImageGenerationMode;
aspect_ratio?: CommonAspectRatio;
resolution?: CommonVideoResolution;
image_batch_count?: number;
}
export interface EstimateImageCostPayload {
success: boolean;
cost_in_credits?: number;
cost_in_usd_cents?: number;
is_free: boolean;
is_unlimited: boolean;
is_rate_limited: boolean;
has_watermark: boolean;
}
export interface EstimateImageCostSuccess extends CommandResult {
payload: EstimateImageCostPayload;
}
export interface EstimateImageCostErrorPayload {
success: boolean;
error_type: "invalid_provider_for_model" | "invalid_input";
error_message: string;
}
export interface EstimateImageCostErrorResult extends CommandResult {
error_details?: EstimateImageCostErrorPayload;
}
export type EstimateImageCostResult =
| EstimateImageCostSuccess
| EstimateImageCostErrorResult;
export const EstimateImageCost = async (
request: EstimateImageCostRequest,
): Promise<EstimateImageCostResult> => {
const result = await invoke("estimate_image_cost_command", { request });
return result as EstimateImageCostResult;
};
export function isEstimateImageCostSuccess(
r: EstimateImageCostResult,
): r is EstimateImageCostSuccess {
return r.status === CommandSuccessStatus.Success;
}