@umituz/react-native-ai-generation-content 1.27.10 → 1.27.12
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/entities/flow-config.types.ts +22 -0
- package/src/domains/generation/domain/feature-config.types.ts +8 -1
- package/src/domains/generation/domain/generation.types.ts +16 -0
- package/src/domains/generation/index.ts +17 -0
- package/src/domains/generation/infrastructure/executors/executor-factory.ts +7 -1
- package/src/domains/generation/infrastructure/executors/text-to-image-executor.ts +173 -0
- package/src/domains/generation/wizard/domain/entities/wizard-config.types.ts +22 -0
- package/src/domains/generation/wizard/index.ts +2 -0
- package/src/domains/generation/wizard/presentation/hooks/useGateStep.ts +138 -0
- package/src/features/text-to-image/domain/types/config.types.ts +2 -0
- package/src/features/text-to-image/presentation/hooks/useGeneration.ts +23 -2
- package/src/presentation/components/buttons/GenerateButton.tsx +10 -3
- package/src/presentation/hooks/useAIFeatureCallbacks.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.12",
|
|
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",
|
|
@@ -5,17 +5,39 @@
|
|
|
5
5
|
|
|
6
6
|
/** Step Types */
|
|
7
7
|
export enum StepType {
|
|
8
|
+
// Gate steps - auth and credits check
|
|
9
|
+
AUTH_GATE = "auth_gate",
|
|
10
|
+
CREDIT_GATE = "credit_gate",
|
|
11
|
+
// Content steps
|
|
8
12
|
CATEGORY_SELECTION = "category_selection",
|
|
9
13
|
SCENARIO_SELECTION = "scenario_selection",
|
|
10
14
|
SCENARIO_PREVIEW = "scenario_preview",
|
|
11
15
|
PARTNER_UPLOAD = "partner_upload",
|
|
12
16
|
TEXT_INPUT = "text_input",
|
|
13
17
|
FEATURE_SELECTION = "feature_selection",
|
|
18
|
+
// Generation steps
|
|
14
19
|
GENERATING = "generating",
|
|
15
20
|
RESULT_PREVIEW = "result_preview",
|
|
16
21
|
CUSTOM = "custom",
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
/** Gate Step Result */
|
|
25
|
+
export type GateResult = "passed" | "blocked" | "pending";
|
|
26
|
+
|
|
27
|
+
/** Auth Gate Configuration */
|
|
28
|
+
export interface AuthGateConfig {
|
|
29
|
+
readonly allowAnonymous?: boolean;
|
|
30
|
+
readonly onAuthRequired?: () => void;
|
|
31
|
+
readonly onAuthSuccess?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Credit Gate Configuration */
|
|
35
|
+
export interface CreditGateConfig {
|
|
36
|
+
readonly requiredCredits: number;
|
|
37
|
+
readonly onCreditsExhausted?: () => void;
|
|
38
|
+
readonly onCreditsAvailable?: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
/** Partner Configuration */
|
|
20
42
|
export interface PartnerConfig {
|
|
21
43
|
readonly partnerId: "A" | "B" | "single";
|
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
// Feature Type Enums
|
|
8
8
|
// ============================================================================
|
|
9
9
|
|
|
10
|
-
export type GenerationType =
|
|
10
|
+
export type GenerationType =
|
|
11
|
+
| "image"
|
|
12
|
+
| "video"
|
|
13
|
+
| "text-to-image"
|
|
14
|
+
| "text-to-video"
|
|
15
|
+
| "image-to-video"
|
|
16
|
+
| "meme";
|
|
11
17
|
|
|
12
18
|
export type InputType =
|
|
13
19
|
| "single-photo"
|
|
14
20
|
| "two-photos"
|
|
21
|
+
| "text-only"
|
|
15
22
|
| "text"
|
|
16
23
|
| "photo-text"
|
|
17
24
|
| "photo-photo-text";
|
|
@@ -33,6 +33,22 @@ export interface ImageGenerationInput {
|
|
|
33
33
|
outputFormat?: "jpeg" | "png" | "webp";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export interface TextToImageInput {
|
|
37
|
+
prompt: string;
|
|
38
|
+
negativePrompt?: string;
|
|
39
|
+
aspectRatio?: string;
|
|
40
|
+
size?: string;
|
|
41
|
+
numImages?: number;
|
|
42
|
+
guidanceScale?: number;
|
|
43
|
+
style?: string;
|
|
44
|
+
outputFormat?: "jpeg" | "png" | "webp";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TextToImageOutput {
|
|
48
|
+
imageUrl: string;
|
|
49
|
+
imageUrls: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
export interface VideoGenerationInput {
|
|
37
53
|
sourceImageBase64: string;
|
|
38
54
|
targetImageBase64?: string;
|
|
@@ -27,7 +27,24 @@ export type {
|
|
|
27
27
|
VideoGenerationOutput,
|
|
28
28
|
MemeGenerationInput,
|
|
29
29
|
MemeGenerationOutput,
|
|
30
|
+
TextToImageInput,
|
|
31
|
+
TextToImageOutput,
|
|
30
32
|
} from "./domain/generation.types";
|
|
31
33
|
|
|
34
|
+
export { ExecutorFactory, type GenerationType as ExecutorGenerationType } from "./infrastructure/executors/executor-factory";
|
|
35
|
+
|
|
32
36
|
export * from "./wizard";
|
|
33
37
|
export * from "./infrastructure/flow";
|
|
38
|
+
|
|
39
|
+
// Flow config types from domain
|
|
40
|
+
export {
|
|
41
|
+
StepType,
|
|
42
|
+
type GateResult,
|
|
43
|
+
type AuthGateConfig,
|
|
44
|
+
type CreditGateConfig,
|
|
45
|
+
type FlowState,
|
|
46
|
+
type FlowActions,
|
|
47
|
+
type FlowCallbacks,
|
|
48
|
+
type FlowConfiguration,
|
|
49
|
+
type StepDefinition,
|
|
50
|
+
} from "../../domain/entities/flow-config.types";
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
import type { GenerationExecutor } from "../../domain/generation.types";
|
|
7
7
|
import { ImageExecutor } from "./image-executor";
|
|
8
8
|
import { VideoExecutor } from "./video-executor";
|
|
9
|
+
import { TextToImageExecutor } from "./text-to-image-executor";
|
|
9
10
|
|
|
10
11
|
declare const __DEV__: boolean;
|
|
11
12
|
|
|
12
|
-
type GenerationType = "image" | "video" | "meme";
|
|
13
|
+
export type GenerationType = "image" | "video" | "text-to-image" | "text-to-video" | "image-to-video" | "meme";
|
|
13
14
|
|
|
14
15
|
export class ExecutorFactory {
|
|
15
16
|
private static executors = new Map<
|
|
@@ -27,7 +28,12 @@ export class ExecutorFactory {
|
|
|
27
28
|
case "image":
|
|
28
29
|
this.executors.set(type, new ImageExecutor());
|
|
29
30
|
break;
|
|
31
|
+
case "text-to-image":
|
|
32
|
+
this.executors.set(type, new TextToImageExecutor());
|
|
33
|
+
break;
|
|
30
34
|
case "video":
|
|
35
|
+
case "text-to-video":
|
|
36
|
+
case "image-to-video":
|
|
31
37
|
this.executors.set(type, new VideoExecutor());
|
|
32
38
|
break;
|
|
33
39
|
case "meme":
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Image Executor
|
|
3
|
+
* Uses provider.run() for quick prompt-based image generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
GenerationExecutor,
|
|
8
|
+
GenerationOptions,
|
|
9
|
+
GenerationResult,
|
|
10
|
+
} from "../../domain/generation.types";
|
|
11
|
+
import { providerRegistry } from "../../../../infrastructure/services/provider-registry.service";
|
|
12
|
+
|
|
13
|
+
declare const __DEV__: boolean;
|
|
14
|
+
|
|
15
|
+
export interface TextToImageInput {
|
|
16
|
+
prompt: string;
|
|
17
|
+
negativePrompt?: string;
|
|
18
|
+
aspectRatio?: string;
|
|
19
|
+
size?: string;
|
|
20
|
+
numImages?: number;
|
|
21
|
+
guidanceScale?: number;
|
|
22
|
+
style?: string;
|
|
23
|
+
outputFormat?: "jpeg" | "png" | "webp";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TextToImageOutput {
|
|
27
|
+
imageUrl: string;
|
|
28
|
+
imageUrls: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class TextToImageExecutor
|
|
32
|
+
implements GenerationExecutor<TextToImageInput, TextToImageOutput>
|
|
33
|
+
{
|
|
34
|
+
async generate(
|
|
35
|
+
model: string,
|
|
36
|
+
input: TextToImageInput,
|
|
37
|
+
options?: GenerationOptions,
|
|
38
|
+
): Promise<GenerationResult<TextToImageOutput>> {
|
|
39
|
+
try {
|
|
40
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
41
|
+
console.log("[TextToImageExecutor] Starting", {
|
|
42
|
+
model,
|
|
43
|
+
promptLength: input.prompt?.length || 0,
|
|
44
|
+
aspectRatio: input.aspectRatio,
|
|
45
|
+
numImages: input.numImages,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const provider = providerRegistry.getActiveProvider();
|
|
50
|
+
|
|
51
|
+
if (!provider?.isInitialized()) {
|
|
52
|
+
return { success: false, error: "AI provider not initialized" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!input.prompt?.trim()) {
|
|
56
|
+
return { success: false, error: "Prompt is required" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
options?.onProgress?.(10);
|
|
60
|
+
|
|
61
|
+
const modelInput = this.buildModelInput(input);
|
|
62
|
+
options?.onProgress?.(20);
|
|
63
|
+
|
|
64
|
+
const result = await provider.run(model, modelInput);
|
|
65
|
+
options?.onProgress?.(90);
|
|
66
|
+
|
|
67
|
+
const extracted = this.extractResult(result);
|
|
68
|
+
options?.onProgress?.(100);
|
|
69
|
+
|
|
70
|
+
if (!extracted?.imageUrl) {
|
|
71
|
+
return { success: false, error: "No image in response" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
75
|
+
console.log("[TextToImageExecutor] Success", {
|
|
76
|
+
imageCount: extracted.imageUrls.length,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { success: true, data: extracted };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
|
|
84
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
85
|
+
console.error("[TextToImageExecutor] Error:", message);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { success: false, error: message };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private buildModelInput(input: TextToImageInput): Record<string, unknown> {
|
|
93
|
+
const {
|
|
94
|
+
prompt,
|
|
95
|
+
negativePrompt,
|
|
96
|
+
aspectRatio,
|
|
97
|
+
size,
|
|
98
|
+
numImages,
|
|
99
|
+
guidanceScale,
|
|
100
|
+
style,
|
|
101
|
+
outputFormat,
|
|
102
|
+
} = input;
|
|
103
|
+
|
|
104
|
+
let finalPrompt = prompt;
|
|
105
|
+
if (style && style !== "none") {
|
|
106
|
+
finalPrompt = `${prompt}, ${style} style`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
prompt: finalPrompt,
|
|
111
|
+
...(negativePrompt && { negative_prompt: negativePrompt }),
|
|
112
|
+
...(aspectRatio && { aspect_ratio: aspectRatio }),
|
|
113
|
+
...(size && { image_size: size }),
|
|
114
|
+
...(numImages && { num_images: numImages }),
|
|
115
|
+
...(guidanceScale && { guidance_scale: guidanceScale }),
|
|
116
|
+
...(outputFormat && { output_format: outputFormat }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private extractResult(
|
|
121
|
+
result: unknown,
|
|
122
|
+
): { imageUrl: string; imageUrls: string[] } | undefined {
|
|
123
|
+
if (typeof result !== "object" || result === null) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const r = result as Record<string, unknown>;
|
|
128
|
+
|
|
129
|
+
// Check nested 'data' object
|
|
130
|
+
if (r.data && typeof r.data === "object") {
|
|
131
|
+
const urls = this.extractImagesFromObject(r.data as Record<string, unknown>);
|
|
132
|
+
if (urls.length > 0) {
|
|
133
|
+
return { imageUrl: urls[0], imageUrls: urls };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check direct images array
|
|
138
|
+
const directUrls = this.extractImagesFromObject(r);
|
|
139
|
+
if (directUrls.length > 0) {
|
|
140
|
+
return { imageUrl: directUrls[0], imageUrls: directUrls };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for imageUrl
|
|
144
|
+
if (typeof r.imageUrl === "string") {
|
|
145
|
+
return { imageUrl: r.imageUrl, imageUrls: [r.imageUrl] };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback: base64
|
|
149
|
+
if (typeof r.imageBase64 === "string") {
|
|
150
|
+
const mimeType = typeof r.mimeType === "string" ? r.mimeType : "image/png";
|
|
151
|
+
const dataUrl = `data:${mimeType};base64,${r.imageBase64}`;
|
|
152
|
+
return { imageUrl: dataUrl, imageUrls: [dataUrl] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private extractImagesFromObject(obj: Record<string, unknown>): string[] {
|
|
159
|
+
if (!Array.isArray(obj.images)) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return obj.images
|
|
164
|
+
.map((img) => {
|
|
165
|
+
if (typeof img === "string") return img;
|
|
166
|
+
if (img && typeof img === "object" && "url" in img) {
|
|
167
|
+
return (img as { url: string }).url;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
})
|
|
171
|
+
.filter((url): url is string => url !== null);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -72,10 +72,32 @@ export interface PreviewStepConfig extends BaseStepConfig {
|
|
|
72
72
|
readonly showContinueButton?: boolean;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Auth Gate Step Configuration
|
|
77
|
+
* Blocks flow if user is not authenticated
|
|
78
|
+
*/
|
|
79
|
+
export interface AuthGateStepConfig extends BaseStepConfig {
|
|
80
|
+
readonly type: "auth_gate";
|
|
81
|
+
readonly allowAnonymous?: boolean;
|
|
82
|
+
readonly messageKey?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Credit Gate Step Configuration
|
|
87
|
+
* Blocks flow if user doesn't have enough credits
|
|
88
|
+
*/
|
|
89
|
+
export interface CreditGateStepConfig extends BaseStepConfig {
|
|
90
|
+
readonly type: "credit_gate";
|
|
91
|
+
readonly requiredCredits: number;
|
|
92
|
+
readonly messageKey?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
/**
|
|
76
96
|
* Union of all step config types
|
|
77
97
|
*/
|
|
78
98
|
export type WizardStepConfig =
|
|
99
|
+
| AuthGateStepConfig
|
|
100
|
+
| CreditGateStepConfig
|
|
79
101
|
| PhotoUploadStepConfig
|
|
80
102
|
| TextInputStepConfig
|
|
81
103
|
| SelectionStepConfig
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGateStep Hook
|
|
3
|
+
* Handles auth and credit gate steps in wizard flow
|
|
4
|
+
*
|
|
5
|
+
* Gates are "invisible" steps that:
|
|
6
|
+
* - Auto-advance when condition is met
|
|
7
|
+
* - Show modal/paywall when blocked
|
|
8
|
+
* - Never show UI themselves
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect, useRef, useCallback } from "react";
|
|
12
|
+
import type { GateResult } from "../../../../../domain/entities/flow-config.types";
|
|
13
|
+
|
|
14
|
+
declare const __DEV__: boolean;
|
|
15
|
+
|
|
16
|
+
export interface GateCallbacks {
|
|
17
|
+
/** Called when auth is required - should show auth modal */
|
|
18
|
+
readonly onAuthRequired?: () => void;
|
|
19
|
+
/** Called when credits are exhausted - should show paywall */
|
|
20
|
+
readonly onCreditsExhausted?: () => void;
|
|
21
|
+
/** Called when gate passes - auto advance to next step */
|
|
22
|
+
readonly onGatePassed?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AuthGateState {
|
|
26
|
+
readonly isAuthenticated: boolean;
|
|
27
|
+
readonly isAnonymous: boolean;
|
|
28
|
+
readonly userId: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CreditGateState {
|
|
32
|
+
readonly creditBalance: number;
|
|
33
|
+
readonly requiredCredits: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseGateStepProps {
|
|
37
|
+
readonly gateType: "auth" | "credit";
|
|
38
|
+
readonly authState?: AuthGateState;
|
|
39
|
+
readonly creditState?: CreditGateState;
|
|
40
|
+
readonly callbacks: GateCallbacks;
|
|
41
|
+
readonly allowAnonymous?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface UseGateStepReturn {
|
|
45
|
+
readonly gateResult: GateResult;
|
|
46
|
+
readonly checkGate: () => GateResult;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Hook to manage gate step logic
|
|
51
|
+
* Automatically triggers callbacks and advances when conditions are met
|
|
52
|
+
*/
|
|
53
|
+
export function useGateStep(props: UseGateStepProps): UseGateStepReturn {
|
|
54
|
+
const { gateType, authState, creditState, callbacks, allowAnonymous = false } = props;
|
|
55
|
+
|
|
56
|
+
const hasChecked = useRef(false);
|
|
57
|
+
|
|
58
|
+
const checkAuthGate = useCallback((): GateResult => {
|
|
59
|
+
if (!authState) return "pending";
|
|
60
|
+
|
|
61
|
+
const { isAuthenticated, isAnonymous, userId } = authState;
|
|
62
|
+
|
|
63
|
+
// Allow anonymous if configured
|
|
64
|
+
if (allowAnonymous && userId) {
|
|
65
|
+
return "passed";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Must be authenticated and not anonymous
|
|
69
|
+
if (isAuthenticated && !isAnonymous && userId) {
|
|
70
|
+
return "passed";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return "blocked";
|
|
74
|
+
}, [authState, allowAnonymous]);
|
|
75
|
+
|
|
76
|
+
const checkCreditGate = useCallback((): GateResult => {
|
|
77
|
+
if (!creditState) return "pending";
|
|
78
|
+
|
|
79
|
+
const { creditBalance, requiredCredits } = creditState;
|
|
80
|
+
|
|
81
|
+
if (creditBalance >= requiredCredits) {
|
|
82
|
+
return "passed";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return "blocked";
|
|
86
|
+
}, [creditState]);
|
|
87
|
+
|
|
88
|
+
const checkGate = useCallback((): GateResult => {
|
|
89
|
+
if (gateType === "auth") {
|
|
90
|
+
return checkAuthGate();
|
|
91
|
+
}
|
|
92
|
+
return checkCreditGate();
|
|
93
|
+
}, [gateType, checkAuthGate, checkCreditGate]);
|
|
94
|
+
|
|
95
|
+
// Auto-check and trigger callbacks on mount
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (hasChecked.current) return;
|
|
98
|
+
|
|
99
|
+
const result = checkGate();
|
|
100
|
+
|
|
101
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
102
|
+
console.log(`[useGateStep] ${gateType} gate check:`, result);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (result === "passed") {
|
|
106
|
+
hasChecked.current = true;
|
|
107
|
+
callbacks.onGatePassed?.();
|
|
108
|
+
} else if (result === "blocked") {
|
|
109
|
+
hasChecked.current = true;
|
|
110
|
+
if (gateType === "auth") {
|
|
111
|
+
callbacks.onAuthRequired?.();
|
|
112
|
+
} else {
|
|
113
|
+
callbacks.onCreditsExhausted?.();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}, [gateType, checkGate, callbacks]);
|
|
117
|
+
|
|
118
|
+
// Re-check when auth/credit state changes
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!hasChecked.current) return;
|
|
121
|
+
|
|
122
|
+
const result = checkGate();
|
|
123
|
+
|
|
124
|
+
if (result === "passed") {
|
|
125
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
126
|
+
console.log(`[useGateStep] ${gateType} gate now passed, advancing...`);
|
|
127
|
+
}
|
|
128
|
+
callbacks.onGatePassed?.();
|
|
129
|
+
}
|
|
130
|
+
}, [authState, creditState, gateType, checkGate, callbacks]);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
gateResult: checkGate(),
|
|
134
|
+
checkGate,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default useGateStep;
|
|
@@ -39,6 +39,8 @@ export type TextToImageGenerationResult =
|
|
|
39
39
|
| TextToImageGenerationResultError;
|
|
40
40
|
|
|
41
41
|
export interface TextToImageCallbacks {
|
|
42
|
+
/** User ID for orchestrator - required for credit deduction */
|
|
43
|
+
userId: string | null;
|
|
42
44
|
executeGeneration: (
|
|
43
45
|
request: TextToImageGenerationRequest,
|
|
44
46
|
) => Promise<TextToImageGenerationResult>;
|
|
@@ -46,7 +46,10 @@ const DEFAULT_ALERT_MESSAGES: AlertMessages = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
export function useGeneration(options: UseGenerationOptions): UseGenerationReturn {
|
|
49
|
-
const { formState, callbacks,
|
|
49
|
+
const { formState, callbacks, onPromptCleared } = options;
|
|
50
|
+
|
|
51
|
+
// Get userId from callbacks (from app layer via useAIFeatureCallbacks)
|
|
52
|
+
const userId = callbacks.userId ?? undefined;
|
|
50
53
|
|
|
51
54
|
const totalCost = callbacks.calculateCost(formState.numImages, formState.selectedModel);
|
|
52
55
|
|
|
@@ -100,6 +103,24 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
|
|
|
100
103
|
return { success: false, error: "Prompt is required" };
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
// Auth check BEFORE generation
|
|
107
|
+
if (!callbacks.isAuthenticated()) {
|
|
108
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
109
|
+
console.log("[TextToImage] Auth required");
|
|
110
|
+
}
|
|
111
|
+
callbacks.onAuthRequired?.();
|
|
112
|
+
return { success: false, error: "Authentication required" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Credit check BEFORE generation
|
|
116
|
+
if (!callbacks.canAfford(totalCost)) {
|
|
117
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
118
|
+
console.log("[TextToImage] Insufficient credits", { totalCost });
|
|
119
|
+
}
|
|
120
|
+
callbacks.onCreditsRequired?.(totalCost);
|
|
121
|
+
return { success: false, error: "Insufficient credits" };
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
const request: TextToImageGenerationRequest = {
|
|
104
125
|
prompt: trimmedPrompt,
|
|
105
126
|
model: formState.selectedModel ?? undefined,
|
|
@@ -120,7 +141,7 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
|
|
|
120
141
|
|
|
121
142
|
// Return result based on orchestrator state
|
|
122
143
|
return null; // Result handled via callbacks
|
|
123
|
-
}, [formState, generate, callbacks]);
|
|
144
|
+
}, [formState, generate, callbacks, totalCost]);
|
|
124
145
|
|
|
125
146
|
return {
|
|
126
147
|
generationState: {
|
|
@@ -86,13 +86,20 @@ export const GenerateButton: React.FC<GenerateButtonProps> = ({
|
|
|
86
86
|
>
|
|
87
87
|
<View style={styles.buttonContent}>
|
|
88
88
|
{isProcessing ? (
|
|
89
|
-
<AtomicSpinner size="sm" color={tokens.colors.textInverse} />
|
|
89
|
+
<AtomicSpinner size="sm" color={disabled ? tokens.colors.textSecondary : tokens.colors.textInverse} />
|
|
90
90
|
) : (
|
|
91
|
-
<AtomicIcon
|
|
91
|
+
<AtomicIcon
|
|
92
|
+
name={icon}
|
|
93
|
+
customSize={iconSize}
|
|
94
|
+
customColor={disabled ? tokens.colors.textSecondary : tokens.colors.textInverse}
|
|
95
|
+
/>
|
|
92
96
|
)}
|
|
93
97
|
<AtomicText
|
|
94
98
|
type="bodyLarge"
|
|
95
|
-
style={[
|
|
99
|
+
style={[
|
|
100
|
+
styles.buttonText,
|
|
101
|
+
{ color: disabled ? tokens.colors.textSecondary : tokens.colors.textInverse }
|
|
102
|
+
]}
|
|
96
103
|
>
|
|
97
104
|
{finalDisplayText}
|
|
98
105
|
</AtomicText>
|
|
@@ -45,6 +45,9 @@ export type AIFeatureGenerationResult =
|
|
|
45
45
|
* Universal callbacks interface that maps to all feature-specific ones
|
|
46
46
|
*/
|
|
47
47
|
export interface AIFeatureCallbacks<TRequest = unknown, TResult = unknown> {
|
|
48
|
+
// User state - needed by orchestrator
|
|
49
|
+
userId: string | null;
|
|
50
|
+
|
|
48
51
|
// TextToImageCallbacks compatible
|
|
49
52
|
executeGeneration: (request: TRequest) => Promise<AIFeatureGenerationResult>;
|
|
50
53
|
calculateCost: (multiplier?: number, _model?: string | null) => number;
|
|
@@ -140,6 +143,7 @@ export function useAIFeatureCallbacks<TRequest = unknown, TResult = unknown>(
|
|
|
140
143
|
|
|
141
144
|
return useMemo(
|
|
142
145
|
() => ({
|
|
146
|
+
userId,
|
|
143
147
|
executeGeneration,
|
|
144
148
|
calculateCost,
|
|
145
149
|
canAfford,
|
|
@@ -153,6 +157,7 @@ export function useAIFeatureCallbacks<TRequest = unknown, TResult = unknown>(
|
|
|
153
157
|
onShowPaywall,
|
|
154
158
|
}),
|
|
155
159
|
[
|
|
160
|
+
userId,
|
|
156
161
|
executeGeneration,
|
|
157
162
|
calculateCost,
|
|
158
163
|
canAfford,
|