@umituz/react-native-ai-generation-content 1.25.12 → 1.25.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.25.12",
3
+ "version": "1.25.14",
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",
@@ -69,4 +69,5 @@ export type {
69
69
  UseWizardGenerationProps,
70
70
  UseWizardGenerationReturn,
71
71
  WizardScenarioData,
72
+ WizardOutputType,
72
73
  } from "./presentation/hooks/useWizardGeneration";
@@ -12,7 +12,7 @@
12
12
  * NO feature-specific code here - everything driven by configuration!
13
13
  */
14
14
 
15
- import React, { useMemo, useCallback, useEffect } from "react";
15
+ import React, { useMemo, useCallback, useEffect, useRef } from "react";
16
16
  import { View, StyleSheet } from "react-native";
17
17
  import { useAppDesignTokens } from "@umituz/react-native-design-system";
18
18
  import { useFlow } from "../../../../infrastructure/flow/useFlow";
@@ -75,6 +75,14 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
75
75
  initialStepIndex: 0,
76
76
  });
77
77
 
78
+ // Handle progress change - memoized to prevent infinite loops
79
+ const handleProgressChange = useCallback(
80
+ (progress: number) => {
81
+ flow.updateProgress(progress);
82
+ },
83
+ [flow],
84
+ );
85
+
78
86
  // Generation hook - handles AI generation automatically
79
87
  const generationHook = useWizardGeneration({
80
88
  scenario: scenario || { id: featureConfig.id, aiPrompt: "" },
@@ -84,12 +92,13 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
84
92
  alertMessages,
85
93
  onSuccess: onGenerationComplete,
86
94
  onError: onGenerationError,
87
- onProgressChange: (progress) => {
88
- flow.updateProgress(progress);
89
- },
95
+ onProgressChange: handleProgressChange,
90
96
  onCreditsExhausted,
91
97
  });
92
98
 
99
+ // Track previous step ID to prevent infinite loops
100
+ const prevStepIdRef = useRef<string>();
101
+
93
102
  // DEBUG logging
94
103
  if (typeof __DEV__ !== "undefined" && __DEV__) {
95
104
  console.log("[GenericWizardFlow] Render", {
@@ -102,18 +111,23 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
102
111
  }
103
112
 
104
113
  // Notify parent when step changes
114
+ // Only call onStepChange when step ID actually changes (not on every object reference change)
105
115
  useEffect(() => {
106
116
  if (flow.currentStep && onStepChange) {
107
- if (typeof __DEV__ !== "undefined" && __DEV__) {
108
- console.log("[GenericWizardFlow] Step changed", {
109
- stepId: flow.currentStep.id,
110
- stepType: flow.currentStep.type,
111
- });
117
+ const currentStepId = flow.currentStep.id;
118
+ // Only notify if step ID changed
119
+ if (prevStepIdRef.current !== currentStepId) {
120
+ prevStepIdRef.current = currentStepId;
121
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
122
+ console.log("[GenericWizardFlow] Step changed", {
123
+ stepId: flow.currentStep.id,
124
+ stepType: flow.currentStep.type,
125
+ });
126
+ }
127
+ onStepChange(flow.currentStep.id, flow.currentStep.type);
112
128
  }
113
- onStepChange(flow.currentStep.id, flow.currentStep.type);
114
129
  }
115
- // eslint-disable-next-line react-hooks/exhaustive-deps
116
- }, [flow.currentStep, flow.currentStepIndex]);
130
+ }, [flow.currentStep, flow.currentStepIndex, onStepChange]);
117
131
 
118
132
  // Handle step continue
119
133
  const handleStepContinue = useCallback(
@@ -17,9 +17,12 @@ import type { AlertMessages } from "../../../../presentation/hooks/generation/or
17
17
 
18
18
  declare const __DEV__: boolean;
19
19
 
20
+ export type WizardOutputType = "image" | "video";
21
+
20
22
  export interface WizardScenarioData {
21
23
  readonly id: string;
22
24
  readonly aiPrompt: string;
25
+ readonly outputType?: WizardOutputType; // "image" for couple-future, "video" for ai-hug/kiss
23
26
  readonly title?: string;
24
27
  readonly description?: string;
25
28
  [key: string]: unknown;
@@ -31,6 +34,14 @@ interface VideoGenerationInput {
31
34
  readonly prompt: string;
32
35
  }
33
36
 
37
+ interface ImageGenerationInput {
38
+ readonly partnerABase64: string;
39
+ readonly partnerBBase64: string;
40
+ readonly prompt: string;
41
+ }
42
+
43
+ type GenerationInput = VideoGenerationInput | ImageGenerationInput;
44
+
34
45
  export interface UseWizardGenerationProps {
35
46
  readonly scenario: WizardScenarioData;
36
47
  readonly wizardData: Record<string, unknown>;
@@ -60,6 +71,78 @@ function getVideoFeatureType(scenarioId: string): VideoFeatureType {
60
71
  return "ai-hug";
61
72
  }
62
73
 
74
+ async function executeImageGeneration(
75
+ input: ImageGenerationInput,
76
+ onProgress?: (progress: number) => void,
77
+ ): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
78
+ const { providerRegistry } = await import("../../../../infrastructure/services/provider-registry.service");
79
+
80
+ const provider = providerRegistry.getActiveProvider();
81
+ if (!provider || !provider.isInitialized()) {
82
+ return { success: false, error: "AI provider not initialized" };
83
+ }
84
+
85
+ try {
86
+ onProgress?.(5);
87
+
88
+ const formatBase64 = (base64: string): string => {
89
+ return base64.startsWith("data:") ? base64 : `data:image/jpeg;base64,${base64}`;
90
+ };
91
+
92
+ const imageUrls = [input.partnerABase64, input.partnerBBase64]
93
+ .filter(Boolean)
94
+ .map(formatBase64);
95
+
96
+ if (imageUrls.length < 2) {
97
+ return { success: false, error: "Two images required" };
98
+ }
99
+
100
+ onProgress?.(10);
101
+
102
+ const enhancedPrompt = `A photorealistic image of a couple. The first person @image1 and the second person @image2. ${input.prompt}. High quality, detailed, professional photography.`;
103
+
104
+ const modelInput = {
105
+ image_urls: imageUrls,
106
+ prompt: enhancedPrompt,
107
+ aspect_ratio: "1:1",
108
+ output_format: "jpeg",
109
+ num_images: 1,
110
+ };
111
+
112
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
113
+ console.log("[useWizardGeneration] Starting image generation");
114
+ }
115
+
116
+ let lastStatus = "";
117
+ const result = await provider.subscribe("fal-ai/nano-banana", modelInput, {
118
+ timeoutMs: 120000,
119
+ onQueueUpdate: (status) => {
120
+ if (status.status === lastStatus) return;
121
+ lastStatus = status.status;
122
+ if (status.status === "IN_QUEUE") onProgress?.(20);
123
+ else if (status.status === "IN_PROGRESS") onProgress?.(50);
124
+ },
125
+ });
126
+
127
+ onProgress?.(90);
128
+
129
+ const rawResult = result as Record<string, unknown>;
130
+ const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
131
+ const imageUrl = data?.images?.[0]?.url;
132
+
133
+ onProgress?.(100);
134
+
135
+ if (!imageUrl) {
136
+ return { success: false, error: "No image generated" };
137
+ }
138
+
139
+ return { success: true, imageUrl };
140
+ } catch (error) {
141
+ const message = error instanceof Error ? error.message : "Generation failed";
142
+ return { success: false, error: message };
143
+ }
144
+ }
145
+
63
146
  async function convertUriToBase64(uri: string): Promise<string> {
64
147
  try {
65
148
  const base64 = await FileSystem.readAsStringAsync(uri, {
@@ -77,7 +160,7 @@ async function convertUriToBase64(uri: string): Promise<string> {
77
160
  async function buildGenerationInput(
78
161
  wizardData: Record<string, unknown>,
79
162
  scenario: WizardScenarioData,
80
- ): Promise<VideoGenerationInput | null> {
163
+ ): Promise<GenerationInput | null> {
81
164
  const photo1Key = Object.keys(wizardData).find((k) => k.includes("photo_1"));
82
165
  const photo2Key = Object.keys(wizardData).find((k) => k.includes("photo_2"));
83
166
 
@@ -110,11 +193,23 @@ async function buildGenerationInput(
110
193
  convertUriToBase64(photo2.uri),
111
194
  ]);
112
195
 
113
- return {
114
- sourceImageBase64: photo1Base64,
115
- targetImageBase64: photo2Base64,
116
- prompt: scenario.aiPrompt || `Generate ${scenario.id} scene`,
117
- };
196
+ const prompt = scenario.aiPrompt || `Generate ${scenario.id} scene`;
197
+ const outputType = scenario.outputType || "video"; // Default to video for backward compatibility
198
+
199
+ // Build input based on output type
200
+ if (outputType === "image") {
201
+ return {
202
+ partnerABase64: photo1Base64,
203
+ partnerBBase64: photo2Base64,
204
+ prompt,
205
+ } as ImageGenerationInput;
206
+ } else {
207
+ return {
208
+ sourceImageBase64: photo1Base64,
209
+ targetImageBase64: photo2Base64,
210
+ prompt,
211
+ } as VideoGenerationInput;
212
+ }
118
213
  }
119
214
 
120
215
  export const useWizardGeneration = (
@@ -133,48 +228,68 @@ export const useWizardGeneration = (
133
228
  } = props;
134
229
 
135
230
  const hasStarted = useRef(false);
136
- const lastInputRef = useRef<VideoGenerationInput | null>(null);
231
+ const lastInputRef = useRef<GenerationInput | null>(null);
137
232
  const repository = useMemo(() => createCreationsRepository("creations"), []);
138
233
  const videoFeatureType = useMemo(() => getVideoFeatureType(scenario.id), [scenario.id]);
234
+ const outputType = scenario.outputType || "video";
235
+
236
+ type GenerationResult = { videoUrl: string } | { imageUrl: string };
139
237
 
140
- const strategy: GenerationStrategy<VideoGenerationInput, { videoUrl: string }> = useMemo(
238
+ const strategy: GenerationStrategy<GenerationInput, GenerationResult> = useMemo(
141
239
  () => ({
142
240
  execute: async (input, onProgress) => {
143
241
  if (typeof __DEV__ !== "undefined" && __DEV__) {
144
242
  console.log("[useWizardGeneration] Executing generation", {
145
243
  scenarioId: scenario.id,
146
- featureType: videoFeatureType,
244
+ outputType,
147
245
  });
148
246
  }
149
247
 
150
248
  lastInputRef.current = input;
151
249
 
152
- const result = await executeVideoFeature(
153
- videoFeatureType,
154
- {
155
- sourceImageBase64: input.sourceImageBase64,
156
- targetImageBase64: input.targetImageBase64,
157
- prompt: input.prompt,
158
- },
159
- { onProgress },
160
- );
161
-
162
- if (!result.success || !result.videoUrl) {
163
- throw new Error(result.error || "Video generation failed");
164
- }
250
+ // Execute based on output type
251
+ if (outputType === "image") {
252
+ const imageInput = input as ImageGenerationInput;
253
+ const result = await executeImageGeneration(imageInput, onProgress);
254
+
255
+ if (!result.success || !result.imageUrl) {
256
+ throw new Error(result.error || "Image generation failed");
257
+ }
258
+
259
+ return { imageUrl: result.imageUrl };
260
+ } else {
261
+ const videoInput = input as VideoGenerationInput;
262
+ const result = await executeVideoFeature(
263
+ videoFeatureType,
264
+ {
265
+ sourceImageBase64: videoInput.sourceImageBase64,
266
+ targetImageBase64: videoInput.targetImageBase64,
267
+ prompt: videoInput.prompt,
268
+ },
269
+ { onProgress },
270
+ );
271
+
272
+ if (!result.success || !result.videoUrl) {
273
+ throw new Error(result.error || "Video generation failed");
274
+ }
165
275
 
166
- return { videoUrl: result.videoUrl };
276
+ return { videoUrl: result.videoUrl };
277
+ }
167
278
  },
168
279
  getCreditCost: () => 1,
169
280
  save: async (result, uid) => {
170
281
  const input = lastInputRef.current;
171
- if (!input || !result.videoUrl) return;
282
+ if (!input) return;
283
+
284
+ const videoResult = result as { videoUrl?: string };
285
+ const imageResult = result as { imageUrl?: string };
172
286
 
173
287
  const creation = {
174
- videoUrl: result.videoUrl,
288
+ videoUrl: videoResult.videoUrl,
289
+ imageUrl: imageResult.imageUrl,
175
290
  scenarioId: scenario.id,
176
291
  scenarioTitle: scenario.title || scenario.id,
177
- prompt: input.prompt,
292
+ prompt: (input as VideoGenerationInput).prompt,
178
293
  createdAt: Date.now(),
179
294
  };
180
295
 
@@ -185,7 +300,7 @@ export const useWizardGeneration = (
185
300
  }
186
301
  },
187
302
  }),
188
- [scenario, videoFeatureType, repository],
303
+ [scenario, videoFeatureType, repository, outputType],
189
304
  );
190
305
 
191
306
  const { generate, isGenerating, progress } = useGenerationOrchestrator(strategy, {