@umituz/react-native-ai-generation-content 1.25.21 → 1.25.24

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.21",
3
+ "version": "1.25.24",
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",
@@ -0,0 +1,2 @@
1
+ export { createWizardStrategy, buildWizardInput } from './wizard-strategy.factory';
2
+ export type { CreateWizardStrategyOptions } from './wizard-strategy.factory';
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Wizard Strategy Factory
3
+ * Creates generation strategies for wizard-based scenarios
4
+ * Centralized strategy creation for all wizard flows
5
+ */
6
+
7
+ import { readFileAsBase64 } from "@umituz/react-native-design-system";
8
+ import type { GenerationStrategy } from "../../../../presentation/hooks/generation/types";
9
+ import type { VideoFeatureType } from "../../../../domain/interfaces";
10
+ import { executeVideoFeature } from "../../../../infrastructure/services/video-feature-executor.service";
11
+ import { createCreationsRepository } from "../../../creations/infrastructure/adapters";
12
+ import { enhanceCouplePrompt } from "../../../../features/couple-future/infrastructure/couplePromptEnhancer";
13
+ import type { CoupleFeatureSelection } from "../../../../features/couple-future/domain/types";
14
+ import type { WizardOutputType, WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
15
+
16
+ declare const __DEV__: boolean;
17
+
18
+ // ============================================================================
19
+ // Types
20
+ // ============================================================================
21
+
22
+ interface ImageGenerationInput {
23
+ readonly partnerABase64: string;
24
+ readonly partnerBBase64: string;
25
+ readonly prompt: string;
26
+ }
27
+
28
+ interface VideoGenerationInput {
29
+ readonly sourceImageBase64: string;
30
+ readonly targetImageBase64: string;
31
+ readonly prompt: string;
32
+ }
33
+
34
+ type WizardGenerationInput = ImageGenerationInput | VideoGenerationInput;
35
+ type WizardGenerationResult = { imageUrl: string } | { videoUrl: string };
36
+
37
+ // ============================================================================
38
+ // Image Generation Executor
39
+ // ============================================================================
40
+
41
+ async function executeImageGeneration(
42
+ input: ImageGenerationInput,
43
+ model: string,
44
+ onProgress?: (progress: number) => void,
45
+ ): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
46
+ const { providerRegistry } = await import("../../../../infrastructure/services/provider-registry.service");
47
+
48
+ const provider = providerRegistry.getActiveProvider();
49
+ if (!provider || !provider.isInitialized()) {
50
+ return { success: false, error: "AI provider not initialized" };
51
+ }
52
+
53
+ try {
54
+ onProgress?.(5);
55
+
56
+ const formatBase64 = (base64: string): string => {
57
+ return base64.startsWith("data:") ? base64 : `data:image/jpeg;base64,${base64}`;
58
+ };
59
+
60
+ const imageUrls = [input.partnerABase64, input.partnerBBase64]
61
+ .filter(Boolean)
62
+ .map(formatBase64);
63
+
64
+ if (imageUrls.length < 2) {
65
+ return { success: false, error: "Two images required" };
66
+ }
67
+
68
+ onProgress?.(10);
69
+
70
+ const enhancedPrompt = `Create a photorealistic image featuring the exact two people from the provided photos. Use the person from @image1 and the person from @image2 exactly as they appear in the reference images - maintain their facial features, expressions, and identity. ${input.prompt}. Professional photography, high quality, detailed, natural lighting, photorealistic rendering.`;
71
+
72
+ const modelInput = {
73
+ image_urls: imageUrls,
74
+ prompt: enhancedPrompt,
75
+ aspect_ratio: "1:1",
76
+ output_format: "jpeg",
77
+ num_images: 1,
78
+ enable_safety_checker: false,
79
+ };
80
+
81
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
82
+ console.log("[WizardStrategy] Starting image generation", { model });
83
+ }
84
+
85
+ let lastStatus = "";
86
+ const result = await provider.subscribe(model, modelInput, {
87
+ timeoutMs: 120000,
88
+ onQueueUpdate: (status) => {
89
+ if (status.status === lastStatus) return;
90
+ lastStatus = status.status;
91
+ if (status.status === "IN_QUEUE") onProgress?.(20);
92
+ else if (status.status === "IN_PROGRESS") onProgress?.(50);
93
+ },
94
+ });
95
+
96
+ onProgress?.(90);
97
+
98
+ const rawResult = result as Record<string, unknown>;
99
+ const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
100
+ const imageUrl = data?.images?.[0]?.url;
101
+
102
+ onProgress?.(100);
103
+
104
+ if (!imageUrl) {
105
+ return { success: false, error: "No image generated" };
106
+ }
107
+
108
+ return { success: true, imageUrl };
109
+ } catch (error) {
110
+ const message = error instanceof Error ? error.message : "Generation failed";
111
+ return { success: false, error: message };
112
+ }
113
+ }
114
+
115
+ // ============================================================================
116
+ // Input Builder
117
+ // ============================================================================
118
+
119
+ async function buildGenerationInput(
120
+ wizardData: Record<string, unknown>,
121
+ scenario: WizardScenarioData,
122
+ ): Promise<WizardGenerationInput | null> {
123
+ const photo1Key = Object.keys(wizardData).find((k) => k.includes("photo_1"));
124
+ const photo2Key = Object.keys(wizardData).find((k) => k.includes("photo_2"));
125
+
126
+ if (!photo1Key || !photo2Key) {
127
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
128
+ console.error("[WizardStrategy] Missing photos in wizard data", {
129
+ keys: Object.keys(wizardData),
130
+ hasPhoto1: !!photo1Key,
131
+ hasPhoto2: !!photo2Key,
132
+ });
133
+ }
134
+ return null;
135
+ }
136
+
137
+ const photo1 = wizardData[photo1Key] as { uri?: string; base64?: string };
138
+ const photo2 = wizardData[photo2Key] as { uri?: string; base64?: string };
139
+
140
+ if (!photo1?.uri || !photo2?.uri) {
141
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
142
+ console.error("[WizardStrategy] Photos missing URI", {
143
+ photo1HasUri: !!photo1?.uri,
144
+ photo2HasUri: !!photo2?.uri,
145
+ });
146
+ }
147
+ return null;
148
+ }
149
+
150
+ // Convert images to base64
151
+ const [photo1Base64, photo2Base64] = await Promise.all([
152
+ readFileAsBase64(photo1.uri),
153
+ readFileAsBase64(photo2.uri),
154
+ ]);
155
+
156
+ if (!photo1Base64 || !photo2Base64) {
157
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
158
+ console.error("[WizardStrategy] Failed to convert images to base64");
159
+ }
160
+ return null;
161
+ }
162
+
163
+ let prompt = scenario.aiPrompt || `Generate ${scenario.id} scene`;
164
+ const outputType = scenario.outputType || "video";
165
+
166
+ // For image generation, enhance prompt with style selections
167
+ if (outputType === "image") {
168
+ const selections: CoupleFeatureSelection = {};
169
+
170
+ // Romantic mood (multi-select array)
171
+ const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
172
+ if (romanticMoods && romanticMoods.length > 0) {
173
+ selections.romanticMoods = romanticMoods;
174
+ }
175
+
176
+ // Art style (single select)
177
+ const artStyle = wizardData.selection_art_style as string | undefined;
178
+ if (artStyle && artStyle !== "original") {
179
+ selections.artStyle = artStyle;
180
+ }
181
+
182
+ // Artist style (single select)
183
+ const artist = wizardData.selection_artist_style as string | undefined;
184
+ if (artist && artist !== "original") {
185
+ selections.artist = artist;
186
+ }
187
+
188
+ // Enhance prompt with selected styles
189
+ if (Object.keys(selections).length > 0) {
190
+ prompt = enhanceCouplePrompt(prompt, selections);
191
+ }
192
+
193
+ return {
194
+ partnerABase64: photo1Base64,
195
+ partnerBBase64: photo2Base64,
196
+ prompt,
197
+ } as ImageGenerationInput;
198
+ } else {
199
+ return {
200
+ sourceImageBase64: photo1Base64,
201
+ targetImageBase64: photo2Base64,
202
+ prompt,
203
+ } as VideoGenerationInput;
204
+ }
205
+ }
206
+
207
+ // ============================================================================
208
+ // Video Feature Type Detection
209
+ // ============================================================================
210
+
211
+ function getVideoFeatureType(scenarioId: string): VideoFeatureType {
212
+ const id = scenarioId.toLowerCase();
213
+
214
+ if (id.includes("kiss")) return "ai-kiss";
215
+ if (id.includes("hug")) return "ai-hug";
216
+
217
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
218
+ console.warn(`[WizardStrategy] Unknown scenario type "${scenarioId}", defaulting to ai-hug`);
219
+ }
220
+ return "ai-hug";
221
+ }
222
+
223
+ // ============================================================================
224
+ // Strategy Factory
225
+ // ============================================================================
226
+
227
+ export interface CreateWizardStrategyOptions {
228
+ readonly scenario: WizardScenarioData;
229
+ readonly wizardData: Record<string, unknown>;
230
+ readonly collectionName?: string;
231
+ }
232
+
233
+ export const createWizardStrategy = (
234
+ options: CreateWizardStrategyOptions,
235
+ ): GenerationStrategy<WizardGenerationInput, WizardGenerationResult> => {
236
+ const { scenario, wizardData, collectionName = "creations" } = options;
237
+ const repository = createCreationsRepository(collectionName);
238
+ const outputType = scenario.outputType || "video";
239
+ const videoFeatureType = getVideoFeatureType(scenario.id);
240
+
241
+ let lastInputRef: WizardGenerationInput | null = null;
242
+
243
+ return {
244
+ execute: async (input, onProgress) => {
245
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
246
+ console.log("[WizardStrategy] Executing generation", {
247
+ scenarioId: scenario.id,
248
+ outputType,
249
+ model: scenario.model,
250
+ });
251
+ }
252
+
253
+ lastInputRef = input;
254
+
255
+ // Execute based on output type
256
+ if (outputType === "image") {
257
+ // Validate model is provided by app
258
+ if (!scenario.model) {
259
+ throw new Error("Model is required for image generation. Please configure model in app generation.config.ts");
260
+ }
261
+
262
+ const imageInput = input as ImageGenerationInput;
263
+ const result = await executeImageGeneration(imageInput, scenario.model, onProgress);
264
+
265
+ if (!result.success || !result.imageUrl) {
266
+ throw new Error(result.error || "Image generation failed");
267
+ }
268
+
269
+ return { imageUrl: result.imageUrl };
270
+ } else {
271
+ const videoInput = input as VideoGenerationInput;
272
+ const result = await executeVideoFeature(
273
+ videoFeatureType,
274
+ {
275
+ sourceImageBase64: videoInput.sourceImageBase64,
276
+ targetImageBase64: videoInput.targetImageBase64,
277
+ prompt: videoInput.prompt,
278
+ },
279
+ { onProgress },
280
+ );
281
+
282
+ if (!result.success || !result.videoUrl) {
283
+ throw new Error(result.error || "Video generation failed");
284
+ }
285
+
286
+ return { videoUrl: result.videoUrl };
287
+ }
288
+ },
289
+
290
+ getCreditCost: () => 1,
291
+
292
+ save: async (result, uid) => {
293
+ const input = lastInputRef;
294
+ if (!input) return;
295
+
296
+ // Validate scenario
297
+ if (!scenario || !scenario.id) {
298
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
299
+ console.error("[WizardStrategy] Cannot save: scenario.id is undefined");
300
+ }
301
+ throw new Error("Scenario ID is required for saving creation");
302
+ }
303
+
304
+ // Extract prompt
305
+ const prompt = 'prompt' in input ? input.prompt : '';
306
+
307
+ // Extract URLs based on output type
308
+ let mainUri = '';
309
+ let imageUrl: string | undefined;
310
+ let videoUrl: string | undefined;
311
+
312
+ if (outputType === "image") {
313
+ const imageResult = result as { imageUrl?: string };
314
+ imageUrl = imageResult.imageUrl;
315
+ mainUri = imageUrl || '';
316
+ } else {
317
+ const videoResult = result as { videoUrl?: string };
318
+ videoUrl = videoResult.videoUrl;
319
+ mainUri = videoUrl || '';
320
+ }
321
+
322
+ // Create unique ID
323
+ const creationId = `${scenario.id}_${Date.now()}`;
324
+
325
+ // Build creation object
326
+ const creation = {
327
+ id: creationId,
328
+ uri: mainUri,
329
+ type: scenario.id,
330
+ prompt,
331
+ createdAt: new Date(),
332
+ isShared: false,
333
+ isFavorite: false,
334
+ metadata: {
335
+ scenarioId: scenario.id,
336
+ scenarioTitle: scenario.title || scenario.id,
337
+ },
338
+ output: {
339
+ imageUrl,
340
+ videoUrl,
341
+ },
342
+ };
343
+
344
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
345
+ console.log("[WizardStrategy] Saving creation", {
346
+ creationId,
347
+ scenarioId: scenario.id,
348
+ outputType,
349
+ });
350
+ }
351
+
352
+ await repository.create(uid, creation);
353
+
354
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
355
+ console.log("[WizardStrategy] Creation saved successfully");
356
+ }
357
+ },
358
+ };
359
+ };
360
+
361
+ // ============================================================================
362
+ // Input Builder Helper (Public API)
363
+ // ============================================================================
364
+
365
+ export const buildWizardInput = buildGenerationInput;
@@ -22,7 +22,7 @@ import { renderStep } from "../../infrastructure/renderers/step-renderer";
22
22
  import type { WizardFeatureConfig } from "../../domain/entities/wizard-config.types";
23
23
  import { buildFlowStepsFromWizard } from "../../infrastructure/builders/dynamic-step-builder";
24
24
  import { useWizardGeneration, type WizardScenarioData } from "../hooks/useWizardGeneration";
25
- import type { AlertMessages } from "../../../../presentation/hooks/generation/orchestrator.types";
25
+ import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
26
26
 
27
27
  export interface GenericWizardFlowProps {
28
28
  readonly featureConfig: WizardFeatureConfig;
@@ -96,9 +96,32 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
96
96
  [updateProgress],
97
97
  );
98
98
 
99
+ // Ensure scenario has required fields - use feature config as fallback
100
+ const validatedScenario = useMemo(() => {
101
+ if (scenario && scenario.id && scenario.aiPrompt !== undefined) {
102
+ return scenario;
103
+ }
104
+
105
+ // Fallback to feature config
106
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
107
+ console.warn("[GenericWizardFlow] Scenario missing required fields, using fallback", {
108
+ hasScenario: !!scenario,
109
+ scenarioId: scenario?.id,
110
+ featureConfigId: featureConfig.id,
111
+ });
112
+ }
113
+
114
+ return {
115
+ id: featureConfig.id,
116
+ aiPrompt: "",
117
+ outputType: "image" as const, // Default to image for safety
118
+ title: featureConfig.id,
119
+ };
120
+ }, [scenario, featureConfig.id]);
121
+
99
122
  // Generation hook - handles AI generation automatically
100
123
  const generationHook = useWizardGeneration({
101
- scenario: scenario || { id: featureConfig.id, aiPrompt: "" },
124
+ scenario: validatedScenario,
102
125
  wizardData: customData,
103
126
  userId,
104
127
  isGeneratingStep: currentStep?.type === StepType.GENERATING,
@@ -110,7 +133,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
110
133
  });
111
134
 
112
135
  // Track previous step ID to prevent infinite loops
113
- const prevStepIdRef = useRef<string>();
136
+ const prevStepIdRef = useRef<string | undefined>(undefined);
114
137
 
115
138
  // DEBUG logging
116
139
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -1,49 +1,31 @@
1
1
  /**
2
2
  * useWizardGeneration Hook
3
- * Generic generation hook for ANY wizard-based scenario
4
- * Handles video generation for couple features (ai-hug, ai-kiss, etc.)
3
+ * Wizard generation using orchestrator + strategy factory pattern
5
4
  */
6
5
 
7
- import { useEffect, useRef, useMemo, useCallback } from "react";
8
- import { readFileAsBase64 } from "@umituz/react-native-design-system";
9
- import {
10
- useGenerationOrchestrator,
11
- type GenerationStrategy,
12
- } from "../../../../presentation/hooks/generation";
13
- import { executeVideoFeature } from "../../../../infrastructure/services/video-feature-executor.service";
14
- import { createCreationsRepository } from "../../../../domains/creations/infrastructure/adapters";
15
- import type { VideoFeatureType } from "../../../../domain/interfaces";
16
- import type { AlertMessages } from "../../../../presentation/hooks/generation/orchestrator.types";
17
- import { enhanceCouplePrompt } from "../../../../features/couple-future/infrastructure/couplePromptEnhancer";
18
- import type { CoupleFeatureSelection } from "../../../../features/couple-future/domain/types";
6
+ import { useEffect, useRef, useMemo } from "react";
7
+ import { useGenerationOrchestrator } from "../../../../presentation/hooks/generation";
8
+ import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
9
+ import { createWizardStrategy, buildWizardInput } from "../../infrastructure/strategies";
19
10
 
20
11
  declare const __DEV__: boolean;
21
12
 
13
+ // ============================================================================
14
+ // Types
15
+ // ============================================================================
16
+
22
17
  export type WizardOutputType = "image" | "video";
23
18
 
24
19
  export interface WizardScenarioData {
25
20
  readonly id: string;
26
21
  readonly aiPrompt: string;
27
- readonly outputType?: WizardOutputType; // "image" for couple-future, "video" for ai-hug/kiss
22
+ readonly outputType?: WizardOutputType;
23
+ readonly model?: string; // AI model from app config
28
24
  readonly title?: string;
29
25
  readonly description?: string;
30
26
  [key: string]: unknown;
31
27
  }
32
28
 
33
- interface VideoGenerationInput {
34
- readonly sourceImageBase64: string;
35
- readonly targetImageBase64: string;
36
- readonly prompt: string;
37
- }
38
-
39
- interface ImageGenerationInput {
40
- readonly partnerABase64: string;
41
- readonly partnerBBase64: string;
42
- readonly prompt: string;
43
- }
44
-
45
- type GenerationInput = VideoGenerationInput | ImageGenerationInput;
46
-
47
29
  export interface UseWizardGenerationProps {
48
30
  readonly scenario: WizardScenarioData;
49
31
  readonly wizardData: Record<string, unknown>;
@@ -61,183 +43,9 @@ export interface UseWizardGenerationReturn {
61
43
  readonly progress: number;
62
44
  }
63
45
 
64
- function getVideoFeatureType(scenarioId: string): VideoFeatureType {
65
- const id = scenarioId.toLowerCase();
66
-
67
- if (id.includes("kiss")) return "ai-kiss";
68
- if (id.includes("hug")) return "ai-hug";
69
-
70
- if (typeof __DEV__ !== "undefined" && __DEV__) {
71
- console.warn(`[useWizardGeneration] Unknown scenario type "${scenarioId}", defaulting to ai-hug`);
72
- }
73
- return "ai-hug";
74
- }
75
-
76
- async function executeImageGeneration(
77
- input: ImageGenerationInput,
78
- onProgress?: (progress: number) => void,
79
- ): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
80
- const { providerRegistry } = await import("../../../../infrastructure/services/provider-registry.service");
81
-
82
- const provider = providerRegistry.getActiveProvider();
83
- if (!provider || !provider.isInitialized()) {
84
- return { success: false, error: "AI provider not initialized" };
85
- }
86
-
87
- try {
88
- onProgress?.(5);
89
-
90
- const formatBase64 = (base64: string): string => {
91
- return base64.startsWith("data:") ? base64 : `data:image/jpeg;base64,${base64}`;
92
- };
93
-
94
- const imageUrls = [input.partnerABase64, input.partnerBBase64]
95
- .filter(Boolean)
96
- .map(formatBase64);
97
-
98
- if (imageUrls.length < 2) {
99
- return { success: false, error: "Two images required" };
100
- }
101
-
102
- onProgress?.(10);
103
-
104
- const enhancedPrompt = `A photorealistic image of a couple. The first person @image1 and the second person @image2. ${input.prompt}. High quality, detailed, professional photography.`;
105
-
106
- const modelInput = {
107
- image_urls: imageUrls,
108
- prompt: enhancedPrompt,
109
- aspect_ratio: "1:1",
110
- output_format: "jpeg",
111
- num_images: 1,
112
- };
113
-
114
- if (typeof __DEV__ !== "undefined" && __DEV__) {
115
- console.log("[useWizardGeneration] Starting image generation");
116
- }
117
-
118
- let lastStatus = "";
119
- const result = await provider.subscribe("fal-ai/nano-banana", modelInput, {
120
- timeoutMs: 120000,
121
- onQueueUpdate: (status) => {
122
- if (status.status === lastStatus) return;
123
- lastStatus = status.status;
124
- if (status.status === "IN_QUEUE") onProgress?.(20);
125
- else if (status.status === "IN_PROGRESS") onProgress?.(50);
126
- },
127
- });
128
-
129
- onProgress?.(90);
130
-
131
- const rawResult = result as Record<string, unknown>;
132
- const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
133
- const imageUrl = data?.images?.[0]?.url;
134
-
135
- onProgress?.(100);
136
-
137
- if (!imageUrl) {
138
- return { success: false, error: "No image generated" };
139
- }
140
-
141
- return { success: true, imageUrl };
142
- } catch (error) {
143
- const message = error instanceof Error ? error.message : "Generation failed";
144
- return { success: false, error: message };
145
- }
146
- }
147
-
148
-
149
- async function buildGenerationInput(
150
- wizardData: Record<string, unknown>,
151
- scenario: WizardScenarioData,
152
- ): Promise<GenerationInput | null> {
153
- const photo1Key = Object.keys(wizardData).find((k) => k.includes("photo_1"));
154
- const photo2Key = Object.keys(wizardData).find((k) => k.includes("photo_2"));
155
-
156
- if (!photo1Key || !photo2Key) {
157
- if (typeof __DEV__ !== "undefined" && __DEV__) {
158
- console.error("[useWizardGeneration] Missing photos in wizard data", {
159
- keys: Object.keys(wizardData),
160
- hasPhoto1: !!photo1Key,
161
- hasPhoto2: !!photo2Key,
162
- });
163
- }
164
- return null;
165
- }
166
-
167
- const photo1 = wizardData[photo1Key] as { uri?: string; base64?: string };
168
- const photo2 = wizardData[photo2Key] as { uri?: string; base64?: string };
169
-
170
- if (!photo1?.uri || !photo2?.uri) {
171
- if (typeof __DEV__ !== "undefined" && __DEV__) {
172
- console.error("[useWizardGeneration] Photos missing URI", {
173
- photo1HasUri: !!photo1?.uri,
174
- photo2HasUri: !!photo2?.uri,
175
- });
176
- }
177
- return null;
178
- }
179
-
180
- // Convert images to base64 using design system utility
181
- const [photo1Base64, photo2Base64] = await Promise.all([
182
- readFileAsBase64(photo1.uri),
183
- readFileAsBase64(photo2.uri),
184
- ]);
185
-
186
- // Validate base64 conversion results
187
- if (!photo1Base64 || !photo2Base64) {
188
- if (typeof __DEV__ !== "undefined" && __DEV__) {
189
- console.error("[useWizardGeneration] Failed to convert images to base64", {
190
- photo1Success: !!photo1Base64,
191
- photo2Success: !!photo2Base64,
192
- });
193
- }
194
- return null;
195
- }
196
-
197
- let prompt = scenario.aiPrompt || `Generate ${scenario.id} scene`;
198
- const outputType = scenario.outputType || "video"; // Default to video for backward compatibility
199
-
200
- // For image generation, enhance prompt with style selections
201
- if (outputType === "image") {
202
- // Extract style selections from wizard data
203
- const selections: CoupleFeatureSelection = {};
204
-
205
- // Romantic mood (multi-select array)
206
- const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
207
- if (romanticMoods && romanticMoods.length > 0) {
208
- selections.romanticMoods = romanticMoods;
209
- }
210
-
211
- // Art style (single select)
212
- const artStyle = wizardData.selection_art_style as string | undefined;
213
- if (artStyle && artStyle !== "original") {
214
- selections.artStyle = artStyle;
215
- }
216
-
217
- // Artist style (single select)
218
- const artist = wizardData.selection_artist_style as string | undefined;
219
- if (artist && artist !== "original") {
220
- selections.artist = artist;
221
- }
222
-
223
- // Enhance prompt with selected styles (only if not "original")
224
- if (Object.keys(selections).length > 0) {
225
- prompt = enhanceCouplePrompt(prompt, selections);
226
- }
227
-
228
- return {
229
- partnerABase64: photo1Base64,
230
- partnerBBase64: photo2Base64,
231
- prompt,
232
- } as ImageGenerationInput;
233
- } else {
234
- return {
235
- sourceImageBase64: photo1Base64,
236
- targetImageBase64: photo2Base64,
237
- prompt,
238
- } as VideoGenerationInput;
239
- }
240
- }
46
+ // ============================================================================
47
+ // Hook
48
+ // ============================================================================
241
49
 
242
50
  export const useWizardGeneration = (
243
51
  props: UseWizardGenerationProps,
@@ -255,119 +63,63 @@ export const useWizardGeneration = (
255
63
  } = props;
256
64
 
257
65
  const hasStarted = useRef(false);
258
- const lastInputRef = useRef<GenerationInput | null>(null);
259
- const repository = useMemo(() => createCreationsRepository("creations"), []);
260
- const videoFeatureType = useMemo(() => getVideoFeatureType(scenario.id), [scenario.id]);
261
- const outputType = scenario.outputType || "video";
262
-
263
- type GenerationResult = { videoUrl: string } | { imageUrl: string };
264
-
265
- const strategy: GenerationStrategy<GenerationInput, GenerationResult> = useMemo(
266
- () => ({
267
- execute: async (input, onProgress) => {
268
- if (typeof __DEV__ !== "undefined" && __DEV__) {
269
- console.log("[useWizardGeneration] Executing generation", {
270
- scenarioId: scenario.id,
271
- outputType,
272
- });
273
- }
274
-
275
- lastInputRef.current = input;
276
-
277
- // Execute based on output type
278
- if (outputType === "image") {
279
- const imageInput = input as ImageGenerationInput;
280
- const result = await executeImageGeneration(imageInput, onProgress);
281
-
282
- if (!result.success || !result.imageUrl) {
283
- throw new Error(result.error || "Image generation failed");
284
- }
285
-
286
- return { imageUrl: result.imageUrl };
287
- } else {
288
- const videoInput = input as VideoGenerationInput;
289
- const result = await executeVideoFeature(
290
- videoFeatureType,
291
- {
292
- sourceImageBase64: videoInput.sourceImageBase64,
293
- targetImageBase64: videoInput.targetImageBase64,
294
- prompt: videoInput.prompt,
295
- },
296
- { onProgress },
297
- );
298
-
299
- if (!result.success || !result.videoUrl) {
300
- throw new Error(result.error || "Video generation failed");
301
- }
302
-
303
- return { videoUrl: result.videoUrl };
304
- }
305
- },
306
- getCreditCost: () => 1,
307
- save: async (result, uid) => {
308
- const input = lastInputRef.current;
309
- if (!input) return;
310
-
311
- // Both input types have prompt field
312
- const prompt = 'prompt' in input ? input.prompt : '';
313
-
314
- // Build creation object with only the relevant URL field
315
- const creation: any = {
316
- scenarioId: scenario.id,
317
- scenarioTitle: scenario.title || scenario.id,
318
- prompt,
319
- createdAt: Date.now(),
320
- };
321
-
322
- // Add only the relevant URL based on output type
323
- if (outputType === "image") {
324
- const imageResult = result as { imageUrl?: string };
325
- creation.imageUrl = imageResult.imageUrl;
326
- } else {
327
- const videoResult = result as { videoUrl?: string };
328
- creation.videoUrl = videoResult.videoUrl;
329
- }
330
66
 
331
- await repository.create(uid, creation);
332
-
333
- if (typeof __DEV__ !== "undefined" && __DEV__) {
334
- console.log("[useWizardGeneration] Creation saved");
335
- }
67
+ // Log output type on mount
68
+ useEffect(() => {
69
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
70
+ console.log("[useWizardGeneration] Initialized", {
71
+ scenarioId: scenario.id,
72
+ outputType: scenario.outputType || "video",
73
+ hasOutputType: !!scenario.outputType,
74
+ });
75
+ }
76
+ }, [scenario.id, scenario.outputType]);
77
+
78
+ // Create strategy using factory
79
+ const strategy = useMemo(() => {
80
+ return createWizardStrategy({
81
+ scenario,
82
+ wizardData,
83
+ collectionName: "creations",
84
+ });
85
+ }, [scenario, wizardData]);
86
+
87
+ // Use orchestrator with strategy
88
+ const { generate, isGenerating, progress } = useGenerationOrchestrator(
89
+ strategy,
90
+ {
91
+ userId,
92
+ alertMessages: alertMessages || {
93
+ networkError: "No internet connection",
94
+ policyViolation: "Content policy violation",
95
+ saveFailed: "Failed to save",
96
+ creditFailed: "Failed to deduct credits",
97
+ unknown: "An error occurred",
336
98
  },
337
- }),
338
- [scenario, videoFeatureType, repository, outputType],
339
- );
340
-
341
- const { generate, isGenerating, progress } = useGenerationOrchestrator(strategy, {
342
- userId,
343
- alertMessages,
344
- onCreditsExhausted,
345
- onSuccess: useCallback(
346
- (result) => {
99
+ onCreditsExhausted,
100
+ onSuccess: (result) => {
347
101
  if (typeof __DEV__ !== "undefined" && __DEV__) {
348
102
  console.log("[useWizardGeneration] Success");
349
103
  }
350
104
  onSuccess?.(result);
351
105
  },
352
- [onSuccess],
353
- ),
354
- onError: useCallback(
355
- (err) => {
106
+ onError: (err) => {
356
107
  if (typeof __DEV__ !== "undefined" && __DEV__) {
357
108
  console.log("[useWizardGeneration] Error:", err.message);
358
109
  }
359
110
  onError?.(err.message);
360
111
  },
361
- [onError],
362
- ),
363
- });
112
+ },
113
+ );
364
114
 
115
+ // Sync progress to parent
365
116
  useEffect(() => {
366
117
  if (onProgressChange) {
367
118
  onProgressChange(progress);
368
119
  }
369
120
  }, [progress, onProgressChange]);
370
121
 
122
+ // Auto-start generation when entering generating step
371
123
  useEffect(() => {
372
124
  if (isGeneratingStep && !hasStarted.current && !isGenerating) {
373
125
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -377,7 +129,8 @@ export const useWizardGeneration = (
377
129
  });
378
130
  }
379
131
 
380
- buildGenerationInput(wizardData, scenario)
132
+ // Build input and start generation
133
+ buildWizardInput(wizardData, scenario)
381
134
  .then((input) => {
382
135
  if (!input) {
383
136
  const error = "Failed to build generation input";
@@ -399,6 +152,7 @@ export const useWizardGeneration = (
399
152
  });
400
153
  }
401
154
 
155
+ // Reset hasStarted when leaving generating step
402
156
  if (!isGeneratingStep && hasStarted.current) {
403
157
  hasStarted.current = false;
404
158
  }