mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 05:20:58 -05:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8052396ca6 | ||
|
|
538192110c | ||
|
|
d48ee450b8 | ||
|
|
374590a918 |
@@ -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";
|
||||
|
||||
+34
@@ -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;
|
||||
}
|
||||
+14
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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 -----
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user