@umituz/react-native-ai-generation-content 1.73.0 → 1.74.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domain/constants/queue-status.constants.ts +1 -1
- package/src/domains/creations/domain/entities/Creation.ts +2 -2
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +2 -2
- package/src/domains/generation/index.ts +1 -1
- package/src/domains/generation/infrastructure/flow/index.ts +1 -1
- package/src/domains/generation/infrastructure/flow/useFlow.ts +0 -6
- package/src/domains/generation/wizard/domain/types/credit-calculation.types.ts +23 -0
- package/src/domains/generation/wizard/index.ts +16 -0
- package/src/domains/generation/wizard/infrastructure/utils/creation-persistence.types.ts +3 -0
- package/src/domains/generation/wizard/infrastructure/utils/creation-save-operations.ts +3 -0
- package/src/domains/generation/wizard/infrastructure/utils/creation-update-operations.ts +1 -1
- package/src/domains/generation/wizard/infrastructure/utils/credit-calculator.ts +48 -0
- package/src/domains/generation/wizard/infrastructure/utils/credit-value-extractors.ts +44 -0
- package/src/domains/generation/wizard/infrastructure/utils/wizard-data-validators.ts +61 -0
- package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +5 -0
- package/src/domains/generation/wizard/presentation/components/WizardContinueButton.tsx +9 -1
- package/src/domains/generation/wizard/presentation/components/WizardFlow.types.ts +3 -0
- package/src/domains/generation/wizard/presentation/components/WizardFlowContent.tsx +42 -2
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +3 -2
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.types.ts +2 -0
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderPhotoUploadStep.tsx +3 -0
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderSelectionStep.tsx +4 -0
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderTextInputStep.tsx +3 -3
- package/src/domains/generation/wizard/presentation/hooks/use-video-queue-generation.types.ts +1 -0
- package/src/domains/generation/wizard/presentation/hooks/useGenerationPhase.ts +1 -1
- package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +10 -0
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +9 -1
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +2 -0
- package/src/domains/generation/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +3 -0
- package/src/domains/generation/wizard/presentation/screens/SelectionScreen.tsx +2 -1
- package/src/domains/generation/wizard/presentation/screens/SelectionScreen.types.ts +2 -0
- package/src/domains/generation/wizard/presentation/screens/TextInputScreen.tsx +21 -2
- package/src/domains/image-to-video/presentation/screens/ImageToVideoWizardFlow.tsx +2 -0
- package/src/domains/scenarios/README.md +3 -3
- package/src/domains/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +2 -0
- package/src/domains/text-to-video/presentation/screens/TextToVideoWizardFlow.tsx +2 -0
- package/src/index.ts +9 -0
- package/src/infrastructure/services/multi-image-generation.executor.ts +1 -3
- package/src/infrastructure/utils/error-classification.ts +1 -1
- package/src/infrastructure/utils/message-extractor.ts +4 -4
- package/src/infrastructure/utils/url-extractor/extraction-rules.ts +6 -6
- package/src/infrastructure/utils/url-extractor/image-result-extractor.ts +0 -1
- package/src/infrastructure/utils/url-extractor/video-result-extractor.ts +0 -1
- package/src/presentation/hooks/generation/useDualImageGeneration.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.74.1",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -35,7 +35,7 @@ export interface Creation {
|
|
|
35
35
|
// Extended fields for job-based creations
|
|
36
36
|
readonly status?: CreationStatus;
|
|
37
37
|
readonly output?: CreationOutput;
|
|
38
|
-
// Background job tracking
|
|
38
|
+
// Background job tracking
|
|
39
39
|
readonly requestId?: string;
|
|
40
40
|
readonly model?: string;
|
|
41
41
|
// Soft delete - if set, the creation is considered deleted
|
|
@@ -61,7 +61,7 @@ export interface CreationDocument {
|
|
|
61
61
|
readonly createdAt: FirebaseTimestamp | Date;
|
|
62
62
|
readonly completedAt?: FirebaseTimestamp | Date | null;
|
|
63
63
|
readonly deletedAt?: FirebaseTimestamp | Date | null;
|
|
64
|
-
// Background job tracking
|
|
64
|
+
// Background job tracking
|
|
65
65
|
readonly requestId?: string;
|
|
66
66
|
readonly model?: string;
|
|
67
67
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useProcessingJobsPoller Hook
|
|
3
|
-
* Polls
|
|
3
|
+
* Polls queue status for "processing" creations and updates Firestore when complete
|
|
4
4
|
* Enables true background generation - works even after wizard is dismissed
|
|
5
|
-
* Uses provider registry internally - no need to pass
|
|
5
|
+
* Uses provider registry internally - no need to pass provider functions
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useEffect, useRef, useMemo } from "react";
|
|
@@ -78,7 +78,7 @@ export {
|
|
|
78
78
|
} from "./wizard";
|
|
79
79
|
|
|
80
80
|
// Flow Infrastructure
|
|
81
|
-
export { createFlowStore, useFlow
|
|
81
|
+
export { createFlowStore, useFlow } from "./infrastructure/flow";
|
|
82
82
|
export type { FlowStoreType } from "./infrastructure/flow";
|
|
83
83
|
|
|
84
84
|
// Flow config types from domain
|
|
@@ -105,9 +105,3 @@ export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
declare const __DEV__: boolean;
|
|
108
|
-
|
|
109
|
-
export const resetFlowStore = () => {
|
|
110
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
111
|
-
console.warn('resetFlowStore is deprecated. Each component now maintains its own flow store instance.');
|
|
112
|
-
}
|
|
113
|
-
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit Calculation Types
|
|
3
|
+
* Domain types for credit calculation system
|
|
4
|
+
* Single Source of Truth for credit-related type definitions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Credit Calculator Function Type
|
|
9
|
+
* Apps provide implementation, package calls it
|
|
10
|
+
*
|
|
11
|
+
* @param params - Calculation parameters
|
|
12
|
+
* @param params.duration - Video duration in seconds
|
|
13
|
+
* @param params.resolution - Video resolution ("480p" | "720p")
|
|
14
|
+
* @param params.outputType - Output type ("video" | "image")
|
|
15
|
+
* @param params.hasImageInput - Whether input includes image (image-to-video)
|
|
16
|
+
* @returns Calculated credit cost
|
|
17
|
+
*/
|
|
18
|
+
export type CreditCalculatorFn = (params: {
|
|
19
|
+
duration?: number;
|
|
20
|
+
resolution?: string;
|
|
21
|
+
outputType: "video" | "image";
|
|
22
|
+
hasImageInput: boolean;
|
|
23
|
+
}) => number;
|
|
@@ -32,6 +32,22 @@ export {
|
|
|
32
32
|
quickBuildWizard,
|
|
33
33
|
} from "./infrastructure/builders/dynamic-step-builder";
|
|
34
34
|
|
|
35
|
+
// Infrastructure - Generic Credit Calculator (USD to Credits conversion ONLY)
|
|
36
|
+
export {
|
|
37
|
+
convertCostToCredits,
|
|
38
|
+
getCreditConfig,
|
|
39
|
+
} from "./infrastructure/utils/credit-calculator";
|
|
40
|
+
|
|
41
|
+
// Infrastructure - Data Validators
|
|
42
|
+
export {
|
|
43
|
+
validateDuration,
|
|
44
|
+
validateResolution,
|
|
45
|
+
} from "./infrastructure/utils/wizard-data-validators";
|
|
46
|
+
export type { ValidationResult } from "./infrastructure/utils/wizard-data-validators";
|
|
47
|
+
|
|
48
|
+
// Credit Calculator Function Type (for apps to implement)
|
|
49
|
+
export type { CreditCalculatorFn } from "./domain/types/credit-calculation.types";
|
|
50
|
+
|
|
35
51
|
// Presentation - Hooks
|
|
36
52
|
export { usePhotoUploadState } from "./presentation/hooks/usePhotoUploadState";
|
|
37
53
|
export type {
|
|
@@ -13,6 +13,9 @@ export interface ProcessingCreationData {
|
|
|
13
13
|
readonly prompt: string;
|
|
14
14
|
readonly requestId?: string;
|
|
15
15
|
readonly model?: string;
|
|
16
|
+
readonly duration?: number;
|
|
17
|
+
readonly resolution?: string;
|
|
18
|
+
readonly creditCost?: number;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface CompletedCreationData {
|
|
@@ -33,6 +33,9 @@ export async function saveAsProcessing(
|
|
|
33
33
|
metadata: {
|
|
34
34
|
scenarioId: data.scenarioId,
|
|
35
35
|
scenarioTitle: data.scenarioTitle,
|
|
36
|
+
...(data.duration && { duration: data.duration }),
|
|
37
|
+
...(data.resolution && { resolution: data.resolution }),
|
|
38
|
+
...(data.creditCost && { creditCost: data.creditCost }),
|
|
36
39
|
},
|
|
37
40
|
});
|
|
38
41
|
|
|
@@ -52,7 +52,7 @@ export async function updateToFailed(
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Update creation with
|
|
55
|
+
* Update creation with queue requestId after job submission
|
|
56
56
|
*/
|
|
57
57
|
export async function updateRequestId(
|
|
58
58
|
repository: ICreationsRepository,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Credit Calculator
|
|
3
|
+
* Package is GENERIC - knows nothing about specific pricing
|
|
4
|
+
* All costs are determined by the APP
|
|
5
|
+
*
|
|
6
|
+
* This package ONLY converts USD cost to credits using app-provided formula
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Credit pricing configuration
|
|
11
|
+
* These are the ONLY constants the package knows about
|
|
12
|
+
*/
|
|
13
|
+
const CREDIT_CONFIG = {
|
|
14
|
+
CUSTOMER_PRICE_PER_CREDIT: 0.10,
|
|
15
|
+
MARKUP_MULTIPLIER: 1.67,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert USD cost to credits
|
|
20
|
+
* This is the ONLY calculation the package does
|
|
21
|
+
* Formula: credits = ceil((cost * markup) / credit_price)
|
|
22
|
+
*
|
|
23
|
+
* @param costInUSD - The cost in USD (provided by app)
|
|
24
|
+
* @throws {Error} If cost is invalid
|
|
25
|
+
*/
|
|
26
|
+
export function convertCostToCredits(costInUSD: number): number {
|
|
27
|
+
if (!costInUSD || typeof costInUSD !== "number" || costInUSD < 0) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`[convertCostToCredits] Invalid cost: ${costInUSD}. Must be a non-negative number.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { CUSTOMER_PRICE_PER_CREDIT, MARKUP_MULTIPLIER } = CREDIT_CONFIG;
|
|
34
|
+
const credits = (costInUSD * MARKUP_MULTIPLIER) / CUSTOMER_PRICE_PER_CREDIT;
|
|
35
|
+
|
|
36
|
+
return Math.ceil(credits);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get credit configuration
|
|
41
|
+
* Exposes config for app to use if needed
|
|
42
|
+
*/
|
|
43
|
+
export function getCreditConfig() {
|
|
44
|
+
return {
|
|
45
|
+
customerPricePerCredit: CREDIT_CONFIG.CUSTOMER_PRICE_PER_CREDIT,
|
|
46
|
+
markupMultiplier: CREDIT_CONFIG.MARKUP_MULTIPLIER,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit Value Extractors
|
|
3
|
+
* Pure utility functions to extract and normalize values from customData
|
|
4
|
+
* Single Responsibility: Data transformation for credit calculation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extract duration value from customData
|
|
9
|
+
* Handles both number and string formats ("4s", "5s", "6s")
|
|
10
|
+
*
|
|
11
|
+
* @param value - Raw value from customData
|
|
12
|
+
* @returns Normalized duration number, or undefined if invalid
|
|
13
|
+
*/
|
|
14
|
+
export function extractDuration(value: unknown): number | undefined {
|
|
15
|
+
// Already a number
|
|
16
|
+
if (typeof value === "number" && value > 0) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// String format: "4s", "5s", "6s" → parse to number
|
|
21
|
+
if (typeof value === "string") {
|
|
22
|
+
const match = value.match(/^(\d+)s?$/);
|
|
23
|
+
if (match) {
|
|
24
|
+
const parsed = parseInt(match[1], 10);
|
|
25
|
+
return parsed > 0 ? parsed : undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract resolution value from customData
|
|
34
|
+
* Validates against allowed values
|
|
35
|
+
*
|
|
36
|
+
* @param value - Raw value from customData
|
|
37
|
+
* @returns Normalized resolution string, or undefined if invalid
|
|
38
|
+
*/
|
|
39
|
+
export function extractResolution(value: unknown): "480p" | "720p" | undefined {
|
|
40
|
+
if (value === "480p" || value === "720p") {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard Data Validators
|
|
3
|
+
* Centralized validation utilities for wizard generation data
|
|
4
|
+
* DRY: Used across AIGenerateScreen, TextToVideoWizardScreen, ImageToVideoWizardScreen
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ValidationResult<T> {
|
|
8
|
+
value?: T;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate and extract duration from wizard data
|
|
14
|
+
*
|
|
15
|
+
* @param data - Wizard generation data
|
|
16
|
+
* @returns Validation result with value or error
|
|
17
|
+
*/
|
|
18
|
+
export function validateDuration(
|
|
19
|
+
data: Record<string, unknown>,
|
|
20
|
+
): ValidationResult<number> {
|
|
21
|
+
const duration = data.duration as number;
|
|
22
|
+
|
|
23
|
+
if (!duration || typeof duration !== "number" || duration <= 0) {
|
|
24
|
+
return {
|
|
25
|
+
error: `Invalid duration: ${duration}. Must be a positive number.`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { value: duration };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate and extract resolution from wizard data
|
|
34
|
+
*
|
|
35
|
+
* @param data - Wizard generation data
|
|
36
|
+
* @returns Validation result with resolution or error
|
|
37
|
+
*/
|
|
38
|
+
export function validateResolution(
|
|
39
|
+
data: Record<string, unknown>,
|
|
40
|
+
): ValidationResult<"480p" | "720p"> {
|
|
41
|
+
const resolutionValue = data.resolution as string;
|
|
42
|
+
|
|
43
|
+
if (!resolutionValue || typeof resolutionValue !== "string") {
|
|
44
|
+
return {
|
|
45
|
+
error: `Invalid resolution: ${resolutionValue}. Must be a string.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Map resolution - EXACT MATCH ONLY
|
|
50
|
+
if (resolutionValue === "480p") {
|
|
51
|
+
return { value: "480p" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (resolutionValue === "720p") {
|
|
55
|
+
return { value: "720p" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
error: `Invalid resolution value: "${resolutionValue}". Must be "480p" or "720p".`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -10,6 +10,7 @@ import type { WizardFeatureConfig } from "../../domain/entities/wizard-feature.t
|
|
|
10
10
|
import type { WizardScenarioData } from "../hooks/useWizardGeneration";
|
|
11
11
|
import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
|
|
12
12
|
import type { GenerationErrorInfo } from "./WizardFlow.types";
|
|
13
|
+
import type { CreditCalculatorFn } from "../../domain/types/credit-calculation.types";
|
|
13
14
|
import { validateScenario } from "../utilities/validateScenario";
|
|
14
15
|
import { WizardFlowContent } from "./WizardFlowContent";
|
|
15
16
|
import {
|
|
@@ -27,6 +28,8 @@ export interface GenericWizardFlowProps {
|
|
|
27
28
|
readonly alertMessages: AlertMessages;
|
|
28
29
|
/** Credit cost for this generation - REQUIRED, determined by the app */
|
|
29
30
|
readonly creditCost: number;
|
|
31
|
+
/** Calculator function provided by APP - package calls this to get dynamic cost */
|
|
32
|
+
readonly calculateCredits?: CreditCalculatorFn;
|
|
30
33
|
readonly skipResultStep?: boolean;
|
|
31
34
|
readonly onStepChange?: (stepId: string, stepType: StepType | string) => void;
|
|
32
35
|
readonly onGenerationStart?: (
|
|
@@ -53,6 +56,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
|
|
|
53
56
|
userId,
|
|
54
57
|
alertMessages,
|
|
55
58
|
creditCost,
|
|
59
|
+
calculateCredits,
|
|
56
60
|
skipResultStep = false,
|
|
57
61
|
onStepChange,
|
|
58
62
|
onGenerationStart,
|
|
@@ -109,6 +113,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
|
|
|
109
113
|
userId={userId}
|
|
110
114
|
alertMessages={alertMessages}
|
|
111
115
|
creditCost={creditCost}
|
|
116
|
+
calculateCredits={calculateCredits}
|
|
112
117
|
skipResultStep={skipResultStep}
|
|
113
118
|
onStepChange={onStepChange}
|
|
114
119
|
onGenerationStart={onGenerationStart}
|
|
@@ -19,6 +19,7 @@ export interface WizardContinueButtonProps {
|
|
|
19
19
|
readonly onPress: () => void;
|
|
20
20
|
readonly label: string;
|
|
21
21
|
readonly icon?: IconName;
|
|
22
|
+
readonly creditCost?: number;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function WizardContinueButton({
|
|
@@ -26,6 +27,7 @@ export function WizardContinueButton({
|
|
|
26
27
|
onPress,
|
|
27
28
|
label,
|
|
28
29
|
icon = "chevron-forward-outline",
|
|
30
|
+
creditCost,
|
|
29
31
|
}: WizardContinueButtonProps) {
|
|
30
32
|
const tokens = useAppDesignTokens();
|
|
31
33
|
const { isTabletDevice, minTouchTarget } = useResponsive();
|
|
@@ -34,6 +36,12 @@ export function WizardContinueButton({
|
|
|
34
36
|
const buttonMinHeight = Math.max(minTouchTarget, 44);
|
|
35
37
|
const buttonMinWidth = isTabletDevice ? 120 : 100;
|
|
36
38
|
|
|
39
|
+
// If creditCost is provided, append it to the label
|
|
40
|
+
const displayLabel =
|
|
41
|
+
creditCost !== undefined && creditCost > 0
|
|
42
|
+
? `${label} (${creditCost} credits)`
|
|
43
|
+
: label;
|
|
44
|
+
|
|
37
45
|
return (
|
|
38
46
|
<TouchableOpacity
|
|
39
47
|
onPress={onPress}
|
|
@@ -60,7 +68,7 @@ export function WizardContinueButton({
|
|
|
60
68
|
{ color: canContinue ? tokens.colors.onPrimary : tokens.colors.textSecondary },
|
|
61
69
|
]}
|
|
62
70
|
>
|
|
63
|
-
{
|
|
71
|
+
{displayLabel}
|
|
64
72
|
</AtomicText>
|
|
65
73
|
<AtomicIcon
|
|
66
74
|
name={icon}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
|
|
6
|
+
import type { CreditCalculatorFn } from "../../domain/types/credit-calculation.types";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Error information with refund eligibility
|
|
@@ -23,6 +24,8 @@ export interface BaseWizardFlowProps {
|
|
|
23
24
|
readonly userId?: string;
|
|
24
25
|
/** Credit cost for this generation - REQUIRED, determined by the app */
|
|
25
26
|
readonly creditCost: number;
|
|
27
|
+
/** Calculator function provided by APP - package calls this to get dynamic cost */
|
|
28
|
+
readonly calculateCredits?: CreditCalculatorFn;
|
|
26
29
|
/** Called when network is unavailable and generation is blocked */
|
|
27
30
|
readonly onNetworkError?: () => void;
|
|
28
31
|
/** Called when generation completes */
|
|
@@ -16,8 +16,10 @@ import type { Creation } from "../../../../creations/domain/entities/Creation";
|
|
|
16
16
|
import { createCreationsRepository } from "../../../../creations";
|
|
17
17
|
import { useResultActions } from "../../../../result-preview/presentation/hooks/useResultActions";
|
|
18
18
|
import { useWizardFlowHandlers } from "../hooks/useWizardFlowHandlers";
|
|
19
|
+
import { extractDuration, extractResolution } from "../../infrastructure/utils/credit-value-extractors";
|
|
19
20
|
import { WizardStepRenderer } from "./WizardStepRenderer";
|
|
20
21
|
import { StarRatingPicker } from "../../../../result-preview/presentation/components/StarRatingPicker";
|
|
22
|
+
import type { CreditCalculatorFn } from "../../domain/types/credit-calculation.types";
|
|
21
23
|
|
|
22
24
|
export interface WizardFlowContentProps {
|
|
23
25
|
readonly featureConfig: WizardFeatureConfig;
|
|
@@ -27,6 +29,8 @@ export interface WizardFlowContentProps {
|
|
|
27
29
|
readonly alertMessages: AlertMessages;
|
|
28
30
|
/** Credit cost for this generation - REQUIRED, determined by the app */
|
|
29
31
|
readonly creditCost: number;
|
|
32
|
+
/** Calculator function provided by APP - package calls this to get dynamic cost */
|
|
33
|
+
readonly calculateCredits?: CreditCalculatorFn;
|
|
30
34
|
readonly skipResultStep?: boolean;
|
|
31
35
|
readonly onStepChange?: (stepId: string, stepType: StepType | string) => void;
|
|
32
36
|
readonly onGenerationStart?: (
|
|
@@ -51,7 +55,8 @@ export const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
|
|
|
51
55
|
validatedScenario,
|
|
52
56
|
userId,
|
|
53
57
|
alertMessages,
|
|
54
|
-
creditCost,
|
|
58
|
+
creditCost, // Still needed for initial feature gate in parent
|
|
59
|
+
calculateCredits, // Calculator function from APP
|
|
55
60
|
skipResultStep = false,
|
|
56
61
|
onStepChange,
|
|
57
62
|
onGenerationStart,
|
|
@@ -103,6 +108,40 @@ export const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
|
|
|
103
108
|
videoUrl: resultVideoUrl,
|
|
104
109
|
});
|
|
105
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Calculate credit cost - CENTRALIZED CALCULATION
|
|
113
|
+
* React Best Practice: Calculate derived state during render (not in useEffect)
|
|
114
|
+
*
|
|
115
|
+
* Flow:
|
|
116
|
+
* 1. Extract values from customData using utility functions
|
|
117
|
+
* 2. Call app's calculator function with normalized values
|
|
118
|
+
* 3. Fallback to static creditCost if calculation incomplete
|
|
119
|
+
*/
|
|
120
|
+
const calculatedCreditCost = useMemo(() => {
|
|
121
|
+
// If no calculator provided, use static creditCost
|
|
122
|
+
if (!calculateCredits) {
|
|
123
|
+
return creditCost;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const outputType = validatedScenario.outputType as "video" | "image";
|
|
127
|
+
const hasImageInput = validatedScenario.inputType !== "text";
|
|
128
|
+
|
|
129
|
+
// Extract and normalize values from customData
|
|
130
|
+
const duration = extractDuration(customData.duration);
|
|
131
|
+
const resolution = extractResolution(customData.resolution);
|
|
132
|
+
|
|
133
|
+
// Call app's calculator
|
|
134
|
+
const result = calculateCredits({
|
|
135
|
+
duration,
|
|
136
|
+
resolution,
|
|
137
|
+
outputType,
|
|
138
|
+
hasImageInput,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// If result is 0 (incomplete selections), use static initial cost
|
|
142
|
+
return result > 0 ? result : creditCost;
|
|
143
|
+
}, [customData, validatedScenario.outputType, validatedScenario.inputType, calculateCredits, creditCost]);
|
|
144
|
+
|
|
106
145
|
const handlers = useWizardFlowHandlers({
|
|
107
146
|
currentStepIndex,
|
|
108
147
|
flowSteps,
|
|
@@ -131,7 +170,7 @@ export const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
|
|
|
131
170
|
userId,
|
|
132
171
|
isGeneratingStep: currentStep?.type === StepType.GENERATING,
|
|
133
172
|
alertMessages,
|
|
134
|
-
creditCost,
|
|
173
|
+
creditCost: calculatedCreditCost,
|
|
135
174
|
onSuccess: handlers.handleGenerationComplete,
|
|
136
175
|
onError: handlers.handleGenerationError,
|
|
137
176
|
onCreditsExhausted,
|
|
@@ -155,6 +194,7 @@ export const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
|
|
|
155
194
|
isSaving={isSaving}
|
|
156
195
|
isSharing={isSharing}
|
|
157
196
|
showRating={Boolean(userId) && !hasRated}
|
|
197
|
+
creditCost={calculatedCreditCost}
|
|
158
198
|
onNext={handlers.handleNextStep}
|
|
159
199
|
onBack={handlers.handleBack}
|
|
160
200
|
onPhotoContinue={handlers.handlePhotoContinue}
|
|
@@ -25,6 +25,7 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
|
|
|
25
25
|
isSaving,
|
|
26
26
|
isSharing,
|
|
27
27
|
showRating = true,
|
|
28
|
+
creditCost,
|
|
28
29
|
onNext,
|
|
29
30
|
onBack,
|
|
30
31
|
onPhotoContinue,
|
|
@@ -89,13 +90,13 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
case StepType.PARTNER_UPLOAD:
|
|
92
|
-
return renderPhotoUploadStep({ key: step.id, step, customData, onBack, onPhotoContinue, t });
|
|
93
|
+
return renderPhotoUploadStep({ key: step.id, step, customData, onBack, onPhotoContinue, t, creditCost });
|
|
93
94
|
|
|
94
95
|
case StepType.TEXT_INPUT:
|
|
95
96
|
return renderTextInputStep({ key: step.id, step, customData, onBack, onPhotoContinue, t, alertMessages });
|
|
96
97
|
|
|
97
98
|
case StepType.FEATURE_SELECTION:
|
|
98
|
-
return renderSelectionStep({ key: step.id, step, customData, onBack, onPhotoContinue, t });
|
|
99
|
+
return renderSelectionStep({ key: step.id, step, customData, onBack, onPhotoContinue, t, creditCost });
|
|
99
100
|
|
|
100
101
|
default:
|
|
101
102
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -12,6 +12,8 @@ export interface WizardStepRendererProps {
|
|
|
12
12
|
readonly isSaving: boolean;
|
|
13
13
|
readonly isSharing: boolean;
|
|
14
14
|
readonly showRating?: boolean;
|
|
15
|
+
/** Calculated credit cost - passed from parent */
|
|
16
|
+
readonly creditCost?: number;
|
|
15
17
|
readonly onNext: () => void;
|
|
16
18
|
readonly onBack: () => void;
|
|
17
19
|
readonly onPhotoContinue: (stepId: string, image: UploadedImage) => void;
|
|
@@ -15,6 +15,7 @@ export interface PhotoUploadStepProps {
|
|
|
15
15
|
readonly onBack: () => void;
|
|
16
16
|
readonly onPhotoContinue: (stepId: string, image: UploadedImage) => void;
|
|
17
17
|
readonly t: (key: string) => string;
|
|
18
|
+
readonly creditCost?: number;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export function renderPhotoUploadStep({
|
|
@@ -23,6 +24,7 @@ export function renderPhotoUploadStep({
|
|
|
23
24
|
onBack,
|
|
24
25
|
onPhotoContinue,
|
|
25
26
|
t,
|
|
27
|
+
creditCost,
|
|
26
28
|
}: PhotoUploadStepProps): React.ReactElement {
|
|
27
29
|
const wizardConfig = getWizardStepConfig(step.config);
|
|
28
30
|
const titleKey = wizardConfig?.titleKey ?? `wizard.steps.${step.id}.title`;
|
|
@@ -46,6 +48,7 @@ export function renderPhotoUploadStep({
|
|
|
46
48
|
uploadFailed: t("common.errors.upload_failed"),
|
|
47
49
|
}}
|
|
48
50
|
t={t}
|
|
51
|
+
creditCost={creditCost}
|
|
49
52
|
onBack={onBack}
|
|
50
53
|
onContinue={(image) => onPhotoContinue(step.id, image)}
|
|
51
54
|
existingImage={existingPhoto}
|
package/src/domains/generation/wizard/presentation/components/step-renderers/renderSelectionStep.tsx
CHANGED
|
@@ -15,6 +15,8 @@ export interface SelectionStepProps {
|
|
|
15
15
|
readonly onBack: () => void;
|
|
16
16
|
readonly onPhotoContinue: (stepId: string, image: UploadedImage) => void;
|
|
17
17
|
readonly t: (key: string) => string;
|
|
18
|
+
/** Calculated credit cost from parent */
|
|
19
|
+
readonly creditCost?: number;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
declare const __DEV__: boolean;
|
|
@@ -25,6 +27,7 @@ export function renderSelectionStep({
|
|
|
25
27
|
onBack,
|
|
26
28
|
onPhotoContinue,
|
|
27
29
|
t,
|
|
30
|
+
creditCost,
|
|
28
31
|
}: SelectionStepProps): React.ReactElement {
|
|
29
32
|
const selectionConfig = getSelectionConfig(step.config);
|
|
30
33
|
const titleKey = selectionConfig?.titleKey ?? `wizard.steps.${step.id}.title`;
|
|
@@ -76,6 +79,7 @@ export function renderSelectionStep({
|
|
|
76
79
|
layout: selectionConfig?.layout,
|
|
77
80
|
}}
|
|
78
81
|
initialValue={initialValue}
|
|
82
|
+
creditCost={creditCost}
|
|
79
83
|
onBack={onBack}
|
|
80
84
|
onContinue={(value) => {
|
|
81
85
|
onPhotoContinue(step.id, { uri: String(value), selection: value, previewUrl: "" } as UploadedImage);
|
package/src/domains/generation/wizard/presentation/components/step-renderers/renderTextInputStep.tsx
CHANGED
|
@@ -48,9 +48,9 @@ export function renderTextInputStep({
|
|
|
48
48
|
contentNotAllowedMessage: alertMessages?.policyViolation || "This type of content is not supported. Please try a different prompt.",
|
|
49
49
|
}}
|
|
50
50
|
config={{
|
|
51
|
-
minLength: textConfig?.minLength
|
|
52
|
-
maxLength: textConfig?.maxLength
|
|
53
|
-
multiline: textConfig?.multiline
|
|
51
|
+
minLength: textConfig?.minLength !== undefined ? textConfig.minLength : 3,
|
|
52
|
+
maxLength: textConfig?.maxLength !== undefined ? textConfig.maxLength : 1000,
|
|
53
|
+
multiline: textConfig?.multiline !== undefined ? textConfig.multiline : true,
|
|
54
54
|
}}
|
|
55
55
|
initialValue={existingText}
|
|
56
56
|
onBack={onBack}
|
package/src/domains/generation/wizard/presentation/hooks/use-video-queue-generation.types.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface UseVideoQueueGenerationProps {
|
|
|
11
11
|
readonly scenario: WizardScenarioData;
|
|
12
12
|
readonly persistence: CreationPersistence;
|
|
13
13
|
readonly strategy: WizardStrategy;
|
|
14
|
+
readonly creditCost?: number;
|
|
14
15
|
readonly onSuccess?: (result: unknown) => void;
|
|
15
16
|
readonly onError?: (error: string) => void;
|
|
16
17
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* useGenerationPhase Hook
|
|
3
3
|
* Derives generation phase from elapsed time for UX feedback
|
|
4
4
|
*
|
|
5
|
-
* Best Practice: Since actual API progress is unknown (
|
|
5
|
+
* Best Practice: Since actual API progress is unknown (queue systems only return IN_QUEUE/IN_PROGRESS),
|
|
6
6
|
* we use time-based phases to provide meaningful feedback to users.
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -20,6 +20,7 @@ export interface UsePhotoBlockingGenerationProps {
|
|
|
20
20
|
readonly scenario: WizardScenarioData;
|
|
21
21
|
readonly persistence: CreationPersistence;
|
|
22
22
|
readonly strategy: WizardStrategy;
|
|
23
|
+
readonly creditCost?: number;
|
|
23
24
|
readonly alertMessages: AlertMessages;
|
|
24
25
|
readonly onSuccess?: (result: unknown) => void;
|
|
25
26
|
readonly onError?: (error: string) => void;
|
|
@@ -38,6 +39,7 @@ export function usePhotoBlockingGeneration(
|
|
|
38
39
|
userId,
|
|
39
40
|
scenario,
|
|
40
41
|
persistence,
|
|
42
|
+
creditCost,
|
|
41
43
|
strategy,
|
|
42
44
|
alertMessages,
|
|
43
45
|
onSuccess,
|
|
@@ -105,10 +107,18 @@ export function usePhotoBlockingGeneration(
|
|
|
105
107
|
// Save to Firestore first
|
|
106
108
|
if (userId && prompt) {
|
|
107
109
|
try {
|
|
110
|
+
// Extract generation parameters from input (for image generation, no duration/resolution)
|
|
111
|
+
const inputData = input as Record<string, unknown>;
|
|
112
|
+
const duration = typeof inputData?.duration === "number" ? inputData.duration : undefined;
|
|
113
|
+
const resolution = typeof inputData?.resolution === "string" ? inputData.resolution : undefined;
|
|
114
|
+
|
|
108
115
|
const creationId = await persistence.saveAsProcessing(userId, {
|
|
109
116
|
scenarioId: scenario.id,
|
|
110
117
|
scenarioTitle: scenario.title || scenario.id,
|
|
111
118
|
prompt,
|
|
119
|
+
duration,
|
|
120
|
+
resolution,
|
|
121
|
+
creditCost,
|
|
112
122
|
});
|
|
113
123
|
creationIdRef.current = creationId;
|
|
114
124
|
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
} from "./use-video-queue-generation.types";
|
|
15
15
|
|
|
16
16
|
export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): UseVideoQueueGenerationReturn {
|
|
17
|
-
const { userId, scenario, persistence, strategy, onSuccess, onError } = props;
|
|
17
|
+
const { userId, scenario, persistence, strategy, creditCost, onSuccess, onError } = props;
|
|
18
18
|
|
|
19
19
|
const creationIdRef = useRef<string | null>(null);
|
|
20
20
|
const requestIdRef = useRef<string | null>(null);
|
|
@@ -153,10 +153,18 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
153
153
|
let creationId: string | null = null;
|
|
154
154
|
if (userId && prompt) {
|
|
155
155
|
try {
|
|
156
|
+
// Extract generation parameters from input
|
|
157
|
+
const inputData = input as Record<string, unknown>;
|
|
158
|
+
const duration = typeof inputData?.duration === "number" ? inputData.duration : undefined;
|
|
159
|
+
const resolution = typeof inputData?.resolution === "string" ? inputData.resolution : undefined;
|
|
160
|
+
|
|
156
161
|
creationId = await persistence.saveAsProcessing(userId, {
|
|
157
162
|
scenarioId: scenario.id,
|
|
158
163
|
scenarioTitle: scenario.title || scenario.id,
|
|
159
164
|
prompt,
|
|
165
|
+
duration,
|
|
166
|
+
resolution,
|
|
167
|
+
creditCost,
|
|
160
168
|
});
|
|
161
169
|
creationIdRef.current = creationId;
|
|
162
170
|
} catch (error) {
|
|
@@ -51,6 +51,7 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
|
|
|
51
51
|
scenario,
|
|
52
52
|
persistence,
|
|
53
53
|
strategy,
|
|
54
|
+
creditCost,
|
|
54
55
|
onSuccess,
|
|
55
56
|
onError,
|
|
56
57
|
});
|
|
@@ -60,6 +61,7 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
|
|
|
60
61
|
scenario,
|
|
61
62
|
persistence,
|
|
62
63
|
strategy,
|
|
64
|
+
creditCost,
|
|
63
65
|
alertMessages,
|
|
64
66
|
onSuccess,
|
|
65
67
|
onError,
|
|
@@ -46,6 +46,7 @@ export interface PhotoUploadScreenProps {
|
|
|
46
46
|
readonly translations: PhotoUploadScreenTranslations;
|
|
47
47
|
readonly t: (key: string) => string;
|
|
48
48
|
readonly config?: PhotoUploadScreenConfig;
|
|
49
|
+
readonly creditCost?: number;
|
|
49
50
|
readonly onBack: () => void;
|
|
50
51
|
readonly onContinue: (image: UploadedImage) => void;
|
|
51
52
|
readonly existingImage?: UploadedImage | null;
|
|
@@ -58,6 +59,7 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
|
58
59
|
translations,
|
|
59
60
|
t,
|
|
60
61
|
config = DEFAULT_CONFIG,
|
|
62
|
+
creditCost,
|
|
61
63
|
onBack,
|
|
62
64
|
onContinue,
|
|
63
65
|
existingImage,
|
|
@@ -114,6 +116,7 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
|
114
116
|
canContinue={canContinue && !!image}
|
|
115
117
|
onPress={handleContinuePress}
|
|
116
118
|
label={translations.continue}
|
|
119
|
+
creditCost={creditCost}
|
|
117
120
|
/>
|
|
118
121
|
}
|
|
119
122
|
/>
|
|
@@ -31,6 +31,7 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
|
|
|
31
31
|
options,
|
|
32
32
|
config,
|
|
33
33
|
initialValue,
|
|
34
|
+
creditCost,
|
|
34
35
|
onBack,
|
|
35
36
|
onContinue,
|
|
36
37
|
}) => {
|
|
@@ -128,7 +129,7 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
|
|
|
128
129
|
title=""
|
|
129
130
|
onBackPress={onBack}
|
|
130
131
|
rightElement={
|
|
131
|
-
<WizardContinueButton canContinue={canContinue} onPress={handleContinue} label={translations.continueButton} icon="arrow-forward" />
|
|
132
|
+
<WizardContinueButton canContinue={canContinue} onPress={handleContinue} label={translations.continueButton} icon="arrow-forward" creditCost={creditCost} />
|
|
132
133
|
}
|
|
133
134
|
/>
|
|
134
135
|
<ScreenLayout scrollable={true} edges={["left", "right"]} hideScrollIndicator={true} contentContainerStyle={styles.scrollContent}>
|
|
@@ -28,6 +28,8 @@ export interface SelectionScreenProps {
|
|
|
28
28
|
readonly options: readonly SelectionOption[];
|
|
29
29
|
readonly config?: SelectionScreenConfig;
|
|
30
30
|
readonly initialValue?: string | string[];
|
|
31
|
+
/** Calculated credit cost - passed from parent */
|
|
32
|
+
readonly creditCost?: number;
|
|
31
33
|
readonly onBack: () => void;
|
|
32
34
|
readonly onContinue: (selectedValue: string | string[]) => void;
|
|
33
35
|
}
|
|
@@ -40,8 +40,27 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
|
|
|
40
40
|
const alert = useAlert();
|
|
41
41
|
const [text, setText] = useState(initialValue);
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Validate config - REQUIRED, NO DEFAULTS
|
|
44
|
+
if (!config) {
|
|
45
|
+
throw new Error("[TextInputScreen] Config is required but was not provided.");
|
|
46
|
+
}
|
|
47
|
+
if (typeof config.minLength !== "number" || config.minLength < 0) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`[TextInputScreen] Invalid minLength: ${config.minLength}. Must be a non-negative number.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (typeof config.maxLength !== "number" || config.maxLength <= 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`[TextInputScreen] Invalid maxLength: ${config.maxLength}. Must be a positive number.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (config.minLength > config.maxLength) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`[TextInputScreen] Invalid config: minLength (${config.minLength}) > maxLength (${config.maxLength}).`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { minLength, maxLength } = config;
|
|
45
64
|
const canContinue = text.trim().length >= minLength;
|
|
46
65
|
|
|
47
66
|
const handleContinue = useCallback(async () => {
|
|
@@ -24,6 +24,7 @@ export const ImageToVideoWizardFlow: React.FC<ImageToVideoWizardFlowProps> = (pr
|
|
|
24
24
|
model,
|
|
25
25
|
userId,
|
|
26
26
|
creditCost,
|
|
27
|
+
calculateCredits,
|
|
27
28
|
onNetworkError,
|
|
28
29
|
onGenerationComplete,
|
|
29
30
|
onGenerationError,
|
|
@@ -70,6 +71,7 @@ export const ImageToVideoWizardFlow: React.FC<ImageToVideoWizardFlowProps> = (pr
|
|
|
70
71
|
userId={userId}
|
|
71
72
|
alertMessages={alertMessages ?? defaultAlerts}
|
|
72
73
|
creditCost={creditCost}
|
|
74
|
+
calculateCredits={calculateCredits}
|
|
73
75
|
skipResultStep={true}
|
|
74
76
|
onGenerationStart={handleGenerationStart}
|
|
75
77
|
onGenerationComplete={handleGenerationComplete}
|
|
@@ -38,7 +38,7 @@ export const APP_SCENARIOS: ScenarioData[] = [
|
|
|
38
38
|
prompt: 'A powerful warrior in fantasy armor, epic lighting...',
|
|
39
39
|
inputType: 'single',
|
|
40
40
|
outputType: 'image',
|
|
41
|
-
model: '
|
|
41
|
+
model: 'your-provider/text-to-image',
|
|
42
42
|
},
|
|
43
43
|
// ... more scenarios
|
|
44
44
|
];
|
|
@@ -121,7 +121,7 @@ const VIDEO_SCENARIOS: ScenarioData[] = [
|
|
|
121
121
|
category: ScenarioCategory.SOLO_CINEMATIC,
|
|
122
122
|
inputType: 'single',
|
|
123
123
|
outputType: 'video',
|
|
124
|
-
model: '
|
|
124
|
+
model: 'your-provider/image-to-video',
|
|
125
125
|
// ... data
|
|
126
126
|
}
|
|
127
127
|
];
|
|
@@ -135,7 +135,7 @@ const SOLO_SCENARIOS: ScenarioData[] = [
|
|
|
135
135
|
category: ScenarioCategory.SOLO_FANTASY,
|
|
136
136
|
inputType: 'single',
|
|
137
137
|
outputType: 'image',
|
|
138
|
-
model: '
|
|
138
|
+
model: 'your-provider/text-to-image',
|
|
139
139
|
// ... data
|
|
140
140
|
}
|
|
141
141
|
];
|
|
@@ -23,6 +23,7 @@ export const TextToImageWizardFlow: React.FC<TextToImageWizardFlowProps> = (prop
|
|
|
23
23
|
model,
|
|
24
24
|
userId,
|
|
25
25
|
creditCost,
|
|
26
|
+
calculateCredits,
|
|
26
27
|
onNetworkError,
|
|
27
28
|
onGenerationComplete,
|
|
28
29
|
onGenerationError,
|
|
@@ -71,6 +72,7 @@ export const TextToImageWizardFlow: React.FC<TextToImageWizardFlowProps> = (prop
|
|
|
71
72
|
userId={userId}
|
|
72
73
|
alertMessages={alertMessages}
|
|
73
74
|
creditCost={creditCost}
|
|
75
|
+
calculateCredits={calculateCredits}
|
|
74
76
|
skipResultStep={true}
|
|
75
77
|
onGenerationStart={handleGenerationStart}
|
|
76
78
|
onGenerationComplete={handleGenerationComplete}
|
|
@@ -24,6 +24,7 @@ export const TextToVideoWizardFlow: React.FC<TextToVideoWizardFlowProps> = (prop
|
|
|
24
24
|
model,
|
|
25
25
|
userId,
|
|
26
26
|
creditCost,
|
|
27
|
+
calculateCredits,
|
|
27
28
|
onNetworkError,
|
|
28
29
|
onGenerationComplete,
|
|
29
30
|
onGenerationError,
|
|
@@ -70,6 +71,7 @@ export const TextToVideoWizardFlow: React.FC<TextToVideoWizardFlowProps> = (prop
|
|
|
70
71
|
userId={userId}
|
|
71
72
|
alertMessages={alertMessages ?? defaultAlerts}
|
|
72
73
|
creditCost={creditCost}
|
|
74
|
+
calculateCredits={calculateCredits}
|
|
73
75
|
skipResultStep={true}
|
|
74
76
|
onGenerationStart={handleGenerationStart}
|
|
75
77
|
onGenerationComplete={handleGenerationComplete}
|
package/src/index.ts
CHANGED
|
@@ -31,3 +31,12 @@ export {
|
|
|
31
31
|
IMAGE_TO_VIDEO_WIZARD_CONFIG,
|
|
32
32
|
} from "./domains/generation/wizard";
|
|
33
33
|
export type { WizardScenarioData } from "./domains/generation/wizard";
|
|
34
|
+
|
|
35
|
+
// Wizard Validators and Credit Utilities
|
|
36
|
+
export {
|
|
37
|
+
validateDuration,
|
|
38
|
+
validateResolution,
|
|
39
|
+
convertCostToCredits,
|
|
40
|
+
getCreditConfig,
|
|
41
|
+
} from "./domains/generation/wizard";
|
|
42
|
+
export type { ValidationResult, CreditCalculatorFn } from "./domains/generation/wizard";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Multi-Image Generation Executor
|
|
3
|
-
* Handles image generation with multiple input images
|
|
4
|
-
* Sends image_urls array as required by FAL AI nano-banana/edit model
|
|
3
|
+
* Handles image generation with multiple input images
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { validateProvider } from "../utils/provider-validator.util";
|
|
@@ -43,7 +42,6 @@ export interface MultiImageGenerationResult {
|
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
44
|
* Execute image generation with multiple input images
|
|
46
|
-
* Sends image_urls array as required by FAL AI API
|
|
47
45
|
*/
|
|
48
46
|
export async function executeMultiImageGeneration(
|
|
49
47
|
input: MultiImageGenerationInput,
|
|
@@ -50,7 +50,7 @@ export function classifyError(error: unknown): AIErrorInfo {
|
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// 422 = Content Policy Violation
|
|
53
|
+
// 422 = Content Policy Violation
|
|
54
54
|
if (statusCode === 422 || matchesPatterns(message, CONTENT_POLICY_PATTERNS)) {
|
|
55
55
|
return logClassification({
|
|
56
56
|
type: AIErrorType.CONTENT_POLICY,
|
|
@@ -38,8 +38,8 @@ export function getErrorTranslationKey(error: unknown): string | null {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Extract error message from
|
|
42
|
-
* Supports: Error instances,
|
|
41
|
+
* Extract error message from API responses
|
|
42
|
+
* Supports: Error instances, API errors, generic objects
|
|
43
43
|
* Returns translation key if available, otherwise original message
|
|
44
44
|
*/
|
|
45
45
|
export function extractErrorMessage(
|
|
@@ -63,10 +63,10 @@ export function extractErrorMessage(
|
|
|
63
63
|
} else if (typeof error === "object" && error !== null) {
|
|
64
64
|
const errObj = error as Record<string, unknown>;
|
|
65
65
|
|
|
66
|
-
//
|
|
66
|
+
// API error format: {detail: [{msg, type, loc}]}
|
|
67
67
|
if (Array.isArray(errObj.detail) && errObj.detail[0]?.msg) {
|
|
68
68
|
const detailType = errObj.detail[0]?.type;
|
|
69
|
-
// Check for content policy in
|
|
69
|
+
// Check for content policy in API response
|
|
70
70
|
if (detailType === "content_policy_violation") {
|
|
71
71
|
return `error.generation.${GenerationErrorType.CONTENT_POLICY}`;
|
|
72
72
|
}
|
|
@@ -16,15 +16,15 @@ export interface ExtractionRule {
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Image extraction rules - checked in order, first success wins
|
|
19
|
-
* Supports
|
|
19
|
+
* Supports various response wrapper formats and direct formats
|
|
20
20
|
*/
|
|
21
21
|
export const IMAGE_EXTRACTION_RULES: readonly ExtractionRule[] = [
|
|
22
|
-
//
|
|
22
|
+
// Data wrapper formats
|
|
23
23
|
{ path: ["data", "image"], description: "data.image (string)" },
|
|
24
24
|
{ path: ["data", "imageUrl"], description: "data.imageUrl" },
|
|
25
25
|
{ path: ["data", "output"], description: "data.output" },
|
|
26
|
-
{ path: ["data", "image", "url"], description: "data.image.url
|
|
27
|
-
{ path: ["data", "images", "0", "url"], description: "data.images[0].url
|
|
26
|
+
{ path: ["data", "image", "url"], description: "data.image.url" },
|
|
27
|
+
{ path: ["data", "images", "0", "url"], description: "data.images[0].url" },
|
|
28
28
|
// Direct formats (no wrapper)
|
|
29
29
|
{ path: ["image"], description: "image (string)" },
|
|
30
30
|
{ path: ["imageUrl"], description: "imageUrl" },
|
|
@@ -35,10 +35,10 @@ export const IMAGE_EXTRACTION_RULES: readonly ExtractionRule[] = [
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Video extraction rules - checked in order, first success wins
|
|
38
|
-
* Supports
|
|
38
|
+
* Supports various response wrapper formats and direct formats
|
|
39
39
|
*/
|
|
40
40
|
export const VIDEO_EXTRACTION_RULES: readonly ExtractionRule[] = [
|
|
41
|
-
//
|
|
41
|
+
// Data wrapper formats
|
|
42
42
|
{ path: ["data", "video"], description: "data.video (string)" },
|
|
43
43
|
{ path: ["data", "videoUrl"], description: "data.videoUrl" },
|
|
44
44
|
{ path: ["data", "video_url"], description: "data.video_url" },
|
|
@@ -14,7 +14,6 @@ export type ImageResultExtractor = (result: unknown) => string | undefined;
|
|
|
14
14
|
/**
|
|
15
15
|
* Extract image URL from AI generation result
|
|
16
16
|
* Uses Chain of Responsibility pattern with declarative rules
|
|
17
|
-
* Supports: FAL.ai wrapper, birefnet, rembg, flux, and direct formats
|
|
18
17
|
*/
|
|
19
18
|
export const extractImageResult: ImageResultExtractor = (result) => {
|
|
20
19
|
return executeRules(result, IMAGE_EXTRACTION_RULES, "ImageExtractor");
|
|
@@ -14,7 +14,6 @@ export type VideoResultExtractor = (result: unknown) => string | undefined;
|
|
|
14
14
|
/**
|
|
15
15
|
* Extract video URL from AI generation result
|
|
16
16
|
* Uses Chain of Responsibility pattern with declarative rules
|
|
17
|
-
* Supports: FAL.ai wrapper, direct formats, nested objects, arrays
|
|
18
17
|
*/
|
|
19
18
|
export const extractVideoResult: VideoResultExtractor = (result) => {
|
|
20
19
|
return executeRules(result, VIDEO_EXTRACTION_RULES, "VideoExtractor");
|