@umituz/react-native-ai-generation-content 1.25.20 → 1.25.23
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/domains/scenarios/configs/wizard-configs.ts +45 -2
- package/src/domains/scenarios/index.ts +2 -0
- package/src/domains/wizard/infrastructure/strategies/index.ts +2 -0
- package/src/domains/wizard/infrastructure/strategies/wizard-strategy.factory.ts +358 -0
- package/src/domains/wizard/presentation/components/GenericWizardFlow.tsx +26 -3
- package/src/domains/wizard/presentation/hooks/useWizardGeneration.ts +54 -301
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.23",
|
|
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",
|
|
@@ -298,21 +298,61 @@ const mergeConfigOverrides = (
|
|
|
298
298
|
};
|
|
299
299
|
};
|
|
300
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Apply configuration options to filter/modify steps
|
|
303
|
+
*/
|
|
304
|
+
const applyConfigOptions = (
|
|
305
|
+
config: WizardFeatureConfig,
|
|
306
|
+
options?: WizardConfigOptions,
|
|
307
|
+
): WizardFeatureConfig => {
|
|
308
|
+
if (!options) return config;
|
|
309
|
+
|
|
310
|
+
let steps = [...config.steps];
|
|
311
|
+
|
|
312
|
+
// Filter out style selection steps if disabled
|
|
313
|
+
if (options.disableStyleSelections) {
|
|
314
|
+
const styleStepIds = ["romantic_mood", "art_style", "artist_style"];
|
|
315
|
+
steps = steps.filter((step) => !styleStepIds.includes(step.id));
|
|
316
|
+
|
|
317
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
318
|
+
console.log("[WizardConfig] Style selections disabled", {
|
|
319
|
+
filteredStepIds: styleStepIds,
|
|
320
|
+
remainingSteps: steps.map((s) => s.id),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
...config,
|
|
327
|
+
steps,
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Configuration Options for Wizard Behavior
|
|
333
|
+
*/
|
|
334
|
+
export interface WizardConfigOptions {
|
|
335
|
+
readonly disableStyleSelections?: boolean; // Disable romantic mood, art style, artist style
|
|
336
|
+
}
|
|
337
|
+
|
|
301
338
|
/**
|
|
302
339
|
* Get wizard config for a scenario
|
|
303
340
|
* 1. Check for explicit config in SCENARIO_WIZARD_CONFIGS
|
|
304
341
|
* 2. Auto-detect feature type and generate config
|
|
305
342
|
* 3. Apply overrides if provided
|
|
343
|
+
* 4. Apply options to disable certain steps
|
|
306
344
|
*
|
|
307
345
|
* This means ALL scenarios work automatically!
|
|
308
346
|
*/
|
|
309
347
|
export const getScenarioWizardConfig = (
|
|
310
348
|
scenarioId: string,
|
|
349
|
+
options?: WizardConfigOptions,
|
|
311
350
|
overrides?: Partial<WizardFeatureConfig>,
|
|
312
351
|
): WizardFeatureConfig => {
|
|
313
352
|
// 1. Explicit config (highest priority)
|
|
314
353
|
if (SCENARIO_WIZARD_CONFIGS[scenarioId]) {
|
|
315
|
-
|
|
354
|
+
const config = mergeConfigOverrides(SCENARIO_WIZARD_CONFIGS[scenarioId], overrides);
|
|
355
|
+
return applyConfigOptions(config, options);
|
|
316
356
|
}
|
|
317
357
|
|
|
318
358
|
// 2. Auto-detect feature type
|
|
@@ -330,7 +370,10 @@ export const getScenarioWizardConfig = (
|
|
|
330
370
|
const config = factory(scenarioId);
|
|
331
371
|
|
|
332
372
|
// 4. Apply overrides
|
|
333
|
-
|
|
373
|
+
const configWithOverrides = mergeConfigOverrides(config, overrides);
|
|
374
|
+
|
|
375
|
+
// 5. Apply options (disable steps)
|
|
376
|
+
return applyConfigOptions(configWithOverrides, options);
|
|
334
377
|
};
|
|
335
378
|
|
|
336
379
|
/**
|
|
@@ -0,0 +1,358 @@
|
|
|
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
|
+
onProgress?: (progress: number) => void,
|
|
44
|
+
): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
|
|
45
|
+
const { providerRegistry } = await import("../../../../infrastructure/services/provider-registry.service");
|
|
46
|
+
|
|
47
|
+
const provider = providerRegistry.getActiveProvider();
|
|
48
|
+
if (!provider || !provider.isInitialized()) {
|
|
49
|
+
return { success: false, error: "AI provider not initialized" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
onProgress?.(5);
|
|
54
|
+
|
|
55
|
+
const formatBase64 = (base64: string): string => {
|
|
56
|
+
return base64.startsWith("data:") ? base64 : `data:image/jpeg;base64,${base64}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const imageUrls = [input.partnerABase64, input.partnerBBase64]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map(formatBase64);
|
|
62
|
+
|
|
63
|
+
if (imageUrls.length < 2) {
|
|
64
|
+
return { success: false, error: "Two images required" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onProgress?.(10);
|
|
68
|
+
|
|
69
|
+
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.`;
|
|
70
|
+
|
|
71
|
+
const modelInput = {
|
|
72
|
+
image_urls: imageUrls,
|
|
73
|
+
prompt: enhancedPrompt,
|
|
74
|
+
aspect_ratio: "1:1",
|
|
75
|
+
output_format: "jpeg",
|
|
76
|
+
num_images: 1,
|
|
77
|
+
enable_safety_checker: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
81
|
+
console.log("[WizardStrategy] Starting image generation");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let lastStatus = "";
|
|
85
|
+
const result = await provider.subscribe("fal-ai/nano-banana", modelInput, {
|
|
86
|
+
timeoutMs: 120000,
|
|
87
|
+
onQueueUpdate: (status) => {
|
|
88
|
+
if (status.status === lastStatus) return;
|
|
89
|
+
lastStatus = status.status;
|
|
90
|
+
if (status.status === "IN_QUEUE") onProgress?.(20);
|
|
91
|
+
else if (status.status === "IN_PROGRESS") onProgress?.(50);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
onProgress?.(90);
|
|
96
|
+
|
|
97
|
+
const rawResult = result as Record<string, unknown>;
|
|
98
|
+
const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
|
|
99
|
+
const imageUrl = data?.images?.[0]?.url;
|
|
100
|
+
|
|
101
|
+
onProgress?.(100);
|
|
102
|
+
|
|
103
|
+
if (!imageUrl) {
|
|
104
|
+
return { success: false, error: "No image generated" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { success: true, imageUrl };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const message = error instanceof Error ? error.message : "Generation failed";
|
|
110
|
+
return { success: false, error: message };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Input Builder
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
async function buildGenerationInput(
|
|
119
|
+
wizardData: Record<string, unknown>,
|
|
120
|
+
scenario: WizardScenarioData,
|
|
121
|
+
): Promise<WizardGenerationInput | null> {
|
|
122
|
+
const photo1Key = Object.keys(wizardData).find((k) => k.includes("photo_1"));
|
|
123
|
+
const photo2Key = Object.keys(wizardData).find((k) => k.includes("photo_2"));
|
|
124
|
+
|
|
125
|
+
if (!photo1Key || !photo2Key) {
|
|
126
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
127
|
+
console.error("[WizardStrategy] Missing photos in wizard data", {
|
|
128
|
+
keys: Object.keys(wizardData),
|
|
129
|
+
hasPhoto1: !!photo1Key,
|
|
130
|
+
hasPhoto2: !!photo2Key,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const photo1 = wizardData[photo1Key] as { uri?: string; base64?: string };
|
|
137
|
+
const photo2 = wizardData[photo2Key] as { uri?: string; base64?: string };
|
|
138
|
+
|
|
139
|
+
if (!photo1?.uri || !photo2?.uri) {
|
|
140
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
141
|
+
console.error("[WizardStrategy] Photos missing URI", {
|
|
142
|
+
photo1HasUri: !!photo1?.uri,
|
|
143
|
+
photo2HasUri: !!photo2?.uri,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Convert images to base64
|
|
150
|
+
const [photo1Base64, photo2Base64] = await Promise.all([
|
|
151
|
+
readFileAsBase64(photo1.uri),
|
|
152
|
+
readFileAsBase64(photo2.uri),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
if (!photo1Base64 || !photo2Base64) {
|
|
156
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
157
|
+
console.error("[WizardStrategy] Failed to convert images to base64");
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let prompt = scenario.aiPrompt || `Generate ${scenario.id} scene`;
|
|
163
|
+
const outputType = scenario.outputType || "video";
|
|
164
|
+
|
|
165
|
+
// For image generation, enhance prompt with style selections
|
|
166
|
+
if (outputType === "image") {
|
|
167
|
+
const selections: CoupleFeatureSelection = {};
|
|
168
|
+
|
|
169
|
+
// Romantic mood (multi-select array)
|
|
170
|
+
const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
|
|
171
|
+
if (romanticMoods && romanticMoods.length > 0) {
|
|
172
|
+
selections.romanticMoods = romanticMoods;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Art style (single select)
|
|
176
|
+
const artStyle = wizardData.selection_art_style as string | undefined;
|
|
177
|
+
if (artStyle && artStyle !== "original") {
|
|
178
|
+
selections.artStyle = artStyle;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Artist style (single select)
|
|
182
|
+
const artist = wizardData.selection_artist_style as string | undefined;
|
|
183
|
+
if (artist && artist !== "original") {
|
|
184
|
+
selections.artist = artist;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Enhance prompt with selected styles
|
|
188
|
+
if (Object.keys(selections).length > 0) {
|
|
189
|
+
prompt = enhanceCouplePrompt(prompt, selections);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
partnerABase64: photo1Base64,
|
|
194
|
+
partnerBBase64: photo2Base64,
|
|
195
|
+
prompt,
|
|
196
|
+
} as ImageGenerationInput;
|
|
197
|
+
} else {
|
|
198
|
+
return {
|
|
199
|
+
sourceImageBase64: photo1Base64,
|
|
200
|
+
targetImageBase64: photo2Base64,
|
|
201
|
+
prompt,
|
|
202
|
+
} as VideoGenerationInput;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Video Feature Type Detection
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
function getVideoFeatureType(scenarioId: string): VideoFeatureType {
|
|
211
|
+
const id = scenarioId.toLowerCase();
|
|
212
|
+
|
|
213
|
+
if (id.includes("kiss")) return "ai-kiss";
|
|
214
|
+
if (id.includes("hug")) return "ai-hug";
|
|
215
|
+
|
|
216
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
217
|
+
console.warn(`[WizardStrategy] Unknown scenario type "${scenarioId}", defaulting to ai-hug`);
|
|
218
|
+
}
|
|
219
|
+
return "ai-hug";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// Strategy Factory
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
export interface CreateWizardStrategyOptions {
|
|
227
|
+
readonly scenario: WizardScenarioData;
|
|
228
|
+
readonly wizardData: Record<string, unknown>;
|
|
229
|
+
readonly collectionName?: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const createWizardStrategy = (
|
|
233
|
+
options: CreateWizardStrategyOptions,
|
|
234
|
+
): GenerationStrategy<WizardGenerationInput, WizardGenerationResult> => {
|
|
235
|
+
const { scenario, wizardData, collectionName = "creations" } = options;
|
|
236
|
+
const repository = createCreationsRepository(collectionName);
|
|
237
|
+
const outputType = scenario.outputType || "video";
|
|
238
|
+
const videoFeatureType = getVideoFeatureType(scenario.id);
|
|
239
|
+
|
|
240
|
+
let lastInputRef: WizardGenerationInput | null = null;
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
execute: async (input, onProgress) => {
|
|
244
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
245
|
+
console.log("[WizardStrategy] Executing generation", {
|
|
246
|
+
scenarioId: scenario.id,
|
|
247
|
+
outputType,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
lastInputRef = input;
|
|
252
|
+
|
|
253
|
+
// Execute based on output type
|
|
254
|
+
if (outputType === "image") {
|
|
255
|
+
const imageInput = input as ImageGenerationInput;
|
|
256
|
+
const result = await executeImageGeneration(imageInput, onProgress);
|
|
257
|
+
|
|
258
|
+
if (!result.success || !result.imageUrl) {
|
|
259
|
+
throw new Error(result.error || "Image generation failed");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { imageUrl: result.imageUrl };
|
|
263
|
+
} else {
|
|
264
|
+
const videoInput = input as VideoGenerationInput;
|
|
265
|
+
const result = await executeVideoFeature(
|
|
266
|
+
videoFeatureType,
|
|
267
|
+
{
|
|
268
|
+
sourceImageBase64: videoInput.sourceImageBase64,
|
|
269
|
+
targetImageBase64: videoInput.targetImageBase64,
|
|
270
|
+
prompt: videoInput.prompt,
|
|
271
|
+
},
|
|
272
|
+
{ onProgress },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (!result.success || !result.videoUrl) {
|
|
276
|
+
throw new Error(result.error || "Video generation failed");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { videoUrl: result.videoUrl };
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
getCreditCost: () => 1,
|
|
284
|
+
|
|
285
|
+
save: async (result, uid) => {
|
|
286
|
+
const input = lastInputRef;
|
|
287
|
+
if (!input) return;
|
|
288
|
+
|
|
289
|
+
// Validate scenario
|
|
290
|
+
if (!scenario || !scenario.id) {
|
|
291
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
292
|
+
console.error("[WizardStrategy] Cannot save: scenario.id is undefined");
|
|
293
|
+
}
|
|
294
|
+
throw new Error("Scenario ID is required for saving creation");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Extract prompt
|
|
298
|
+
const prompt = 'prompt' in input ? input.prompt : '';
|
|
299
|
+
|
|
300
|
+
// Extract URLs based on output type
|
|
301
|
+
let mainUri = '';
|
|
302
|
+
let imageUrl: string | undefined;
|
|
303
|
+
let videoUrl: string | undefined;
|
|
304
|
+
|
|
305
|
+
if (outputType === "image") {
|
|
306
|
+
const imageResult = result as { imageUrl?: string };
|
|
307
|
+
imageUrl = imageResult.imageUrl;
|
|
308
|
+
mainUri = imageUrl || '';
|
|
309
|
+
} else {
|
|
310
|
+
const videoResult = result as { videoUrl?: string };
|
|
311
|
+
videoUrl = videoResult.videoUrl;
|
|
312
|
+
mainUri = videoUrl || '';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Create unique ID
|
|
316
|
+
const creationId = `${scenario.id}_${Date.now()}`;
|
|
317
|
+
|
|
318
|
+
// Build creation object
|
|
319
|
+
const creation = {
|
|
320
|
+
id: creationId,
|
|
321
|
+
uri: mainUri,
|
|
322
|
+
type: scenario.id,
|
|
323
|
+
prompt,
|
|
324
|
+
createdAt: new Date(),
|
|
325
|
+
isShared: false,
|
|
326
|
+
isFavorite: false,
|
|
327
|
+
metadata: {
|
|
328
|
+
scenarioId: scenario.id,
|
|
329
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
330
|
+
},
|
|
331
|
+
output: {
|
|
332
|
+
imageUrl,
|
|
333
|
+
videoUrl,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
338
|
+
console.log("[WizardStrategy] Saving creation", {
|
|
339
|
+
creationId,
|
|
340
|
+
scenarioId: scenario.id,
|
|
341
|
+
outputType,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await repository.create(uid, creation);
|
|
346
|
+
|
|
347
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
348
|
+
console.log("[WizardStrategy] Creation saved successfully");
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Input Builder Helper (Public API)
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
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/
|
|
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:
|
|
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,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useWizardGeneration Hook
|
|
3
|
-
*
|
|
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
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
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;
|
|
22
|
+
readonly outputType?: WizardOutputType;
|
|
28
23
|
readonly title?: string;
|
|
29
24
|
readonly description?: string;
|
|
30
25
|
[key: string]: unknown;
|
|
31
26
|
}
|
|
32
27
|
|
|
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
28
|
export interface UseWizardGenerationProps {
|
|
48
29
|
readonly scenario: WizardScenarioData;
|
|
49
30
|
readonly wizardData: Record<string, unknown>;
|
|
@@ -61,183 +42,9 @@ export interface UseWizardGenerationReturn {
|
|
|
61
42
|
readonly progress: number;
|
|
62
43
|
}
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
}
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Hook
|
|
47
|
+
// ============================================================================
|
|
241
48
|
|
|
242
49
|
export const useWizardGeneration = (
|
|
243
50
|
props: UseWizardGenerationProps,
|
|
@@ -255,119 +62,63 @@ export const useWizardGeneration = (
|
|
|
255
62
|
} = props;
|
|
256
63
|
|
|
257
64
|
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
65
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
66
|
+
// Log output type on mount
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
69
|
+
console.log("[useWizardGeneration] Initialized", {
|
|
70
|
+
scenarioId: scenario.id,
|
|
71
|
+
outputType: scenario.outputType || "video",
|
|
72
|
+
hasOutputType: !!scenario.outputType,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}, [scenario.id, scenario.outputType]);
|
|
76
|
+
|
|
77
|
+
// Create strategy using factory
|
|
78
|
+
const strategy = useMemo(() => {
|
|
79
|
+
return createWizardStrategy({
|
|
80
|
+
scenario,
|
|
81
|
+
wizardData,
|
|
82
|
+
collectionName: "creations",
|
|
83
|
+
});
|
|
84
|
+
}, [scenario, wizardData]);
|
|
85
|
+
|
|
86
|
+
// Use orchestrator with strategy
|
|
87
|
+
const { generate, isGenerating, progress } = useGenerationOrchestrator(
|
|
88
|
+
strategy,
|
|
89
|
+
{
|
|
90
|
+
userId,
|
|
91
|
+
alertMessages: alertMessages || {
|
|
92
|
+
networkError: "No internet connection",
|
|
93
|
+
policyViolation: "Content policy violation",
|
|
94
|
+
saveFailed: "Failed to save",
|
|
95
|
+
creditFailed: "Failed to deduct credits",
|
|
96
|
+
unknown: "An error occurred",
|
|
336
97
|
},
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
const { generate, isGenerating, progress } = useGenerationOrchestrator(strategy, {
|
|
342
|
-
userId,
|
|
343
|
-
alertMessages,
|
|
344
|
-
onCreditsExhausted,
|
|
345
|
-
onSuccess: useCallback(
|
|
346
|
-
(result) => {
|
|
98
|
+
onCreditsExhausted,
|
|
99
|
+
onSuccess: (result) => {
|
|
347
100
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
348
101
|
console.log("[useWizardGeneration] Success");
|
|
349
102
|
}
|
|
350
103
|
onSuccess?.(result);
|
|
351
104
|
},
|
|
352
|
-
|
|
353
|
-
),
|
|
354
|
-
onError: useCallback(
|
|
355
|
-
(err) => {
|
|
105
|
+
onError: (err) => {
|
|
356
106
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
357
107
|
console.log("[useWizardGeneration] Error:", err.message);
|
|
358
108
|
}
|
|
359
109
|
onError?.(err.message);
|
|
360
110
|
},
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
});
|
|
111
|
+
},
|
|
112
|
+
);
|
|
364
113
|
|
|
114
|
+
// Sync progress to parent
|
|
365
115
|
useEffect(() => {
|
|
366
116
|
if (onProgressChange) {
|
|
367
117
|
onProgressChange(progress);
|
|
368
118
|
}
|
|
369
119
|
}, [progress, onProgressChange]);
|
|
370
120
|
|
|
121
|
+
// Auto-start generation when entering generating step
|
|
371
122
|
useEffect(() => {
|
|
372
123
|
if (isGeneratingStep && !hasStarted.current && !isGenerating) {
|
|
373
124
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -377,7 +128,8 @@ export const useWizardGeneration = (
|
|
|
377
128
|
});
|
|
378
129
|
}
|
|
379
130
|
|
|
380
|
-
|
|
131
|
+
// Build input and start generation
|
|
132
|
+
buildWizardInput(wizardData, scenario)
|
|
381
133
|
.then((input) => {
|
|
382
134
|
if (!input) {
|
|
383
135
|
const error = "Failed to build generation input";
|
|
@@ -399,6 +151,7 @@ export const useWizardGeneration = (
|
|
|
399
151
|
});
|
|
400
152
|
}
|
|
401
153
|
|
|
154
|
+
// Reset hasStarted when leaving generating step
|
|
402
155
|
if (!isGeneratingStep && hasStarted.current) {
|
|
403
156
|
hasStarted.current = false;
|
|
404
157
|
}
|