@umituz/react-native-ai-generation-content 1.33.1 → 1.34.0
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/generation/infrastructure/executors/video-executor.ts +2 -6
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +118 -3
- package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +23 -0
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +7 -1
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.types.ts +2 -0
- package/src/domains/generation/wizard/presentation/screens/GeneratingScreen.tsx +44 -4
- package/src/infrastructure/services/video-feature-executor.service.ts +68 -4
- package/src/presentation/layouts/DualImageFeatureLayout.tsx +17 -2
- package/src/presentation/layouts/DualImageVideoFeatureLayout.tsx +17 -2
- package/src/presentation/layouts/SingleImageFeatureLayout.tsx +18 -2
- package/src/presentation/layouts/SingleImageWithPromptFeatureLayout.tsx +17 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Video Generation Executor
|
|
3
|
-
* Generic executor for all video generation features
|
|
3
|
+
* Generic executor for all video generation features (provider-agnostic)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type {
|
|
@@ -41,16 +41,12 @@ export class VideoExecutor
|
|
|
41
41
|
return { success: false, error: "AI provider not initialized" };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
options?.onProgress?.(5);
|
|
45
|
-
|
|
46
44
|
const modelInput = this.buildModelInput(input);
|
|
47
45
|
|
|
48
46
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
49
47
|
console.log("[VideoExecutor] Model input prepared");
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
options?.onProgress?.(10);
|
|
53
|
-
|
|
54
50
|
const result = await provider.subscribe(model, modelInput, {
|
|
55
51
|
timeoutMs: options?.timeoutMs ?? 300000,
|
|
56
52
|
onQueueUpdate: (status) => {
|
|
@@ -101,7 +97,7 @@ export class VideoExecutor
|
|
|
101
97
|
}
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
private buildModelInput(input: VideoGenerationInput) {
|
|
100
|
+
private buildModelInput(input: VideoGenerationInput): Record<string, unknown> {
|
|
105
101
|
const { sourceImageBase64, targetImageBase64, prompt } = input;
|
|
106
102
|
|
|
107
103
|
const formatBase64 = (base64: string): string => {
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -43,11 +43,24 @@ export interface VideoGenerationResult {
|
|
|
43
43
|
async function extractPhotosFromWizardData(
|
|
44
44
|
wizardData: Record<string, unknown>,
|
|
45
45
|
): Promise<string[]> {
|
|
46
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
|
+
console.log("[VideoStrategy:extractPhotos] Starting extraction", {
|
|
48
|
+
wizardDataKeys: Object.keys(wizardData),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
const photoKeys = Object.keys(wizardData)
|
|
47
53
|
.filter((k) => k.includes(PHOTO_KEY_PREFIX))
|
|
48
54
|
.sort();
|
|
49
55
|
|
|
56
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
57
|
+
console.log("[VideoStrategy:extractPhotos] Found photo keys", { photoKeys });
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
if (photoKeys.length === 0) {
|
|
61
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
62
|
+
console.log("[VideoStrategy:extractPhotos] No photo keys found");
|
|
63
|
+
}
|
|
51
64
|
return [];
|
|
52
65
|
}
|
|
53
66
|
|
|
@@ -56,15 +69,35 @@ async function extractPhotosFromWizardData(
|
|
|
56
69
|
const photo = wizardData[key] as { uri?: string };
|
|
57
70
|
if (photo?.uri) {
|
|
58
71
|
photoUris.push(photo.uri);
|
|
72
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
73
|
+
console.log("[VideoStrategy:extractPhotos] Found photo URI", { key, uri: photo.uri.substring(0, 50) + "..." });
|
|
74
|
+
}
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
if (photoUris.length === 0) {
|
|
79
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
80
|
+
console.log("[VideoStrategy:extractPhotos] No photo URIs found");
|
|
81
|
+
}
|
|
63
82
|
return [];
|
|
64
83
|
}
|
|
65
84
|
|
|
85
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
86
|
+
console.log("[VideoStrategy:extractPhotos] Converting to base64", { count: photoUris.length });
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
|
|
67
|
-
|
|
90
|
+
const validPhotos = photosBase64.filter(Boolean) as string[];
|
|
91
|
+
|
|
92
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
93
|
+
console.log("[VideoStrategy:extractPhotos] Converted photos", {
|
|
94
|
+
total: photoUris.length,
|
|
95
|
+
valid: validPhotos.length,
|
|
96
|
+
sizes: validPhotos.map((p) => `${(p.length / 1024).toFixed(1)}KB`),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return validPhotos;
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
// ============================================================================
|
|
@@ -91,20 +124,52 @@ export async function buildVideoInput(
|
|
|
91
124
|
wizardData: Record<string, unknown>,
|
|
92
125
|
scenario: WizardScenarioData,
|
|
93
126
|
): Promise<VideoGenerationInput | null> {
|
|
127
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
128
|
+
console.log("[VideoStrategy:buildInput] START", {
|
|
129
|
+
scenarioId: scenario.id,
|
|
130
|
+
outputType: scenario.outputType,
|
|
131
|
+
hasAiPrompt: !!scenario.aiPrompt,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
94
135
|
const photos = await extractPhotosFromWizardData(wizardData);
|
|
95
136
|
|
|
137
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
138
|
+
console.log("[VideoStrategy:buildInput] Photos extracted", {
|
|
139
|
+
count: photos.length,
|
|
140
|
+
hasSource: !!photos[0],
|
|
141
|
+
hasTarget: !!photos[1],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
96
145
|
// Extract prompt using type-safe extractor with fallback
|
|
97
146
|
let prompt = extractPrompt(wizardData, scenario.aiPrompt);
|
|
98
147
|
|
|
148
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
149
|
+
console.log("[VideoStrategy:buildInput] Prompt from wizard", {
|
|
150
|
+
hasPrompt: !!prompt,
|
|
151
|
+
promptLength: prompt?.length ?? 0,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
99
155
|
// For video processing features, use default prompt if none provided
|
|
100
156
|
if (!prompt) {
|
|
101
157
|
const defaultPrompt = VIDEO_PROCESSING_PROMPTS[scenario.id];
|
|
102
158
|
if (defaultPrompt) {
|
|
103
159
|
prompt = defaultPrompt;
|
|
104
160
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
105
|
-
console.log("[VideoStrategy] Using default prompt
|
|
161
|
+
console.log("[VideoStrategy:buildInput] Using default prompt", {
|
|
162
|
+
scenarioId: scenario.id,
|
|
163
|
+
prompt: prompt.substring(0, 50) + "...",
|
|
164
|
+
});
|
|
106
165
|
}
|
|
107
166
|
} else {
|
|
167
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
168
|
+
console.log("[VideoStrategy:buildInput] ERROR: No prompt available", {
|
|
169
|
+
scenarioId: scenario.id,
|
|
170
|
+
availablePrompts: Object.keys(VIDEO_PROCESSING_PROMPTS),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
108
173
|
throw new Error("Prompt is required for video generation");
|
|
109
174
|
}
|
|
110
175
|
}
|
|
@@ -114,7 +179,7 @@ export async function buildVideoInput(
|
|
|
114
179
|
const aspectRatio = extractAspectRatio(wizardData);
|
|
115
180
|
const resolution = extractResolution(wizardData);
|
|
116
181
|
|
|
117
|
-
|
|
182
|
+
const input: VideoGenerationInput = {
|
|
118
183
|
sourceImageBase64: photos[0],
|
|
119
184
|
targetImageBase64: photos[1] || photos[0],
|
|
120
185
|
prompt,
|
|
@@ -122,6 +187,21 @@ export async function buildVideoInput(
|
|
|
122
187
|
aspectRatio,
|
|
123
188
|
resolution,
|
|
124
189
|
};
|
|
190
|
+
|
|
191
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
192
|
+
console.log("[VideoStrategy:buildInput] COMPLETE", {
|
|
193
|
+
hasSource: !!input.sourceImageBase64,
|
|
194
|
+
hasTarget: !!input.targetImageBase64,
|
|
195
|
+
sourceSize: input.sourceImageBase64 ? `${(input.sourceImageBase64.length / 1024).toFixed(1)}KB` : "N/A",
|
|
196
|
+
targetSize: input.targetImageBase64 ? `${(input.targetImageBase64.length / 1024).toFixed(1)}KB` : "N/A",
|
|
197
|
+
prompt: input.prompt.substring(0, 50) + "...",
|
|
198
|
+
duration: input.duration,
|
|
199
|
+
aspectRatio: input.aspectRatio,
|
|
200
|
+
resolution: input.resolution,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return input;
|
|
125
205
|
}
|
|
126
206
|
|
|
127
207
|
// ============================================================================
|
|
@@ -138,6 +218,14 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
138
218
|
const repository = createCreationsRepository(collectionName);
|
|
139
219
|
const videoFeatureType = getVideoFeatureType(scenario.id);
|
|
140
220
|
|
|
221
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
222
|
+
console.log("[VideoStrategy:create] Created strategy", {
|
|
223
|
+
scenarioId: scenario.id,
|
|
224
|
+
videoFeatureType,
|
|
225
|
+
collectionName,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
141
229
|
let lastInputRef: VideoGenerationInput | null = null;
|
|
142
230
|
|
|
143
231
|
return {
|
|
@@ -145,6 +233,15 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
145
233
|
const videoInput = input as VideoGenerationInput;
|
|
146
234
|
lastInputRef = videoInput;
|
|
147
235
|
|
|
236
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
237
|
+
console.log("[VideoStrategy:execute] START", {
|
|
238
|
+
featureType: videoFeatureType,
|
|
239
|
+
hasSource: !!videoInput.sourceImageBase64,
|
|
240
|
+
hasTarget: !!videoInput.targetImageBase64,
|
|
241
|
+
prompt: videoInput.prompt.substring(0, 50) + "...",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
148
245
|
const result = await executeVideoFeature(
|
|
149
246
|
videoFeatureType,
|
|
150
247
|
{
|
|
@@ -159,10 +256,28 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
159
256
|
},
|
|
160
257
|
);
|
|
161
258
|
|
|
259
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
260
|
+
console.log("[VideoStrategy:execute] RESULT", {
|
|
261
|
+
success: result.success,
|
|
262
|
+
hasVideoUrl: !!result.videoUrl,
|
|
263
|
+
error: result.error,
|
|
264
|
+
requestId: result.requestId,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
162
268
|
if (!result.success || !result.videoUrl) {
|
|
269
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
270
|
+
console.log("[VideoStrategy:execute] FAILED", { error: result.error });
|
|
271
|
+
}
|
|
163
272
|
throw new Error(result.error || "Video generation failed");
|
|
164
273
|
}
|
|
165
274
|
|
|
275
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
276
|
+
console.log("[VideoStrategy:execute] SUCCESS", {
|
|
277
|
+
videoUrl: result.videoUrl.substring(0, 80) + "...",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
166
281
|
return { videoUrl: result.videoUrl };
|
|
167
282
|
},
|
|
168
283
|
|
|
@@ -66,6 +66,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
66
66
|
const [currentCreation, setCurrentCreation] = useState<Creation | null>(null);
|
|
67
67
|
const [showRatingPicker, setShowRatingPicker] = useState(false);
|
|
68
68
|
const [hasRated, setHasRated] = useState(false);
|
|
69
|
+
const [isGeneratingDismissed, setIsGeneratingDismissed] = useState(false);
|
|
69
70
|
const prevStepIdRef = useRef<string | undefined>(undefined);
|
|
70
71
|
|
|
71
72
|
const repository = useMemo(() => createCreationsRepository("creations"), []);
|
|
@@ -133,10 +134,31 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
133
134
|
if (prevStepIdRef.current !== currentStepId) {
|
|
134
135
|
prevStepIdRef.current = currentStepId;
|
|
135
136
|
onStepChange(currentStep.id, currentStep.type);
|
|
137
|
+
// Reset dismissed state when entering generating step
|
|
138
|
+
if (currentStep.type === StepType.GENERATING) {
|
|
139
|
+
setIsGeneratingDismissed(false);
|
|
140
|
+
}
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
}, [currentStep, currentStepIndex, onStepChange]);
|
|
139
144
|
|
|
145
|
+
// Handle dismiss generating - go back but generation continues in background
|
|
146
|
+
const handleDismissGenerating = useCallback(() => {
|
|
147
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
148
|
+
console.log("[GenericWizardFlow] Dismissing generating screen - generation continues in background");
|
|
149
|
+
}
|
|
150
|
+
setIsGeneratingDismissed(true);
|
|
151
|
+
// Show alert that generation continues
|
|
152
|
+
alert.show(
|
|
153
|
+
AlertType.INFO,
|
|
154
|
+
AlertMode.TOAST,
|
|
155
|
+
t("generator.backgroundTitle"),
|
|
156
|
+
t("generator.backgroundMessage")
|
|
157
|
+
);
|
|
158
|
+
// Go back to previous step (or close)
|
|
159
|
+
onBack?.();
|
|
160
|
+
}, [alert, t, onBack]);
|
|
161
|
+
|
|
140
162
|
const handleBack = useCallback(() => {
|
|
141
163
|
if (currentStepIndex === 0) {
|
|
142
164
|
onBack?.();
|
|
@@ -209,6 +231,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
209
231
|
onShare={handleShare}
|
|
210
232
|
onRate={handleOpenRatingPicker}
|
|
211
233
|
onTryAgain={onTryAgain}
|
|
234
|
+
onDismissGenerating={handleDismissGenerating}
|
|
212
235
|
t={t}
|
|
213
236
|
renderPreview={renderPreview}
|
|
214
237
|
renderGenerating={renderGenerating}
|
|
@@ -28,6 +28,7 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
|
|
|
28
28
|
onShare,
|
|
29
29
|
onRate,
|
|
30
30
|
onTryAgain,
|
|
31
|
+
onDismissGenerating,
|
|
31
32
|
t,
|
|
32
33
|
renderPreview,
|
|
33
34
|
renderGenerating,
|
|
@@ -61,7 +62,12 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
|
|
|
61
62
|
case StepType.GENERATING: {
|
|
62
63
|
if (renderGenerating) return renderGenerating(generationProgress);
|
|
63
64
|
return (
|
|
64
|
-
<GeneratingScreen
|
|
65
|
+
<GeneratingScreen
|
|
66
|
+
progress={generationProgress}
|
|
67
|
+
scenario={scenario}
|
|
68
|
+
t={t}
|
|
69
|
+
onDismiss={onDismissGenerating}
|
|
70
|
+
/>
|
|
65
71
|
);
|
|
66
72
|
}
|
|
67
73
|
|
|
@@ -18,6 +18,8 @@ export interface WizardStepRendererProps {
|
|
|
18
18
|
readonly onShare: () => void;
|
|
19
19
|
readonly onRate?: () => void;
|
|
20
20
|
readonly onTryAgain?: () => void;
|
|
21
|
+
/** Called when user dismisses generating screen - generation continues in background */
|
|
22
|
+
readonly onDismissGenerating?: () => void;
|
|
21
23
|
readonly t: (key: string) => string;
|
|
22
24
|
readonly renderPreview?: (onContinue: () => void) => React.ReactElement | null;
|
|
23
25
|
readonly renderGenerating?: (progress: number) => React.ReactElement | null;
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Generic Generating Screen
|
|
3
3
|
* Shows indeterminate progress while AI generates content
|
|
4
4
|
* Uses status messages instead of fake percentages (UX best practice)
|
|
5
|
+
* Supports background generation - user can dismiss and generation continues
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import React, { useMemo } from "react";
|
|
8
|
-
import { View, StyleSheet, ActivityIndicator } from "react-native";
|
|
9
|
-
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
+
import { View, StyleSheet, ActivityIndicator, TouchableOpacity } from "react-native";
|
|
10
|
+
import { useAppDesignTokens, AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
|
|
10
11
|
import { useGenerationPhase } from "../hooks/useGenerationPhase";
|
|
11
12
|
import { IndeterminateProgressBar } from "../components/IndeterminateProgressBar";
|
|
12
13
|
|
|
@@ -20,17 +21,19 @@ export interface GeneratingScreenProps {
|
|
|
20
21
|
readonly title?: string;
|
|
21
22
|
readonly waitMessage?: string;
|
|
22
23
|
readonly hint?: string;
|
|
24
|
+
readonly backgroundHint?: string;
|
|
23
25
|
};
|
|
24
26
|
};
|
|
25
27
|
readonly t: (key: string) => string;
|
|
26
|
-
|
|
28
|
+
/** Called when user dismisses the screen - generation continues in background */
|
|
29
|
+
readonly onDismiss?: () => void;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
30
33
|
progress: _progress,
|
|
31
34
|
scenario,
|
|
32
35
|
t,
|
|
33
|
-
|
|
36
|
+
onDismiss,
|
|
34
37
|
}) => {
|
|
35
38
|
const tokens = useAppDesignTokens();
|
|
36
39
|
const phase = useGenerationPhase();
|
|
@@ -48,6 +51,7 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
|
48
51
|
title: custom?.title || t("generator.title"),
|
|
49
52
|
waitMessage: custom?.waitMessage || t("generator.waitMessage"),
|
|
50
53
|
hint: custom?.hint || t("generator.hint"),
|
|
54
|
+
backgroundHint: custom?.backgroundHint || t("generator.backgroundHint"),
|
|
51
55
|
};
|
|
52
56
|
}, [scenario, t]);
|
|
53
57
|
|
|
@@ -66,6 +70,17 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
|
66
70
|
|
|
67
71
|
return (
|
|
68
72
|
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
73
|
+
{/* Close button - allows user to dismiss and continue in background */}
|
|
74
|
+
{onDismiss && (
|
|
75
|
+
<TouchableOpacity
|
|
76
|
+
style={styles.closeButton}
|
|
77
|
+
onPress={onDismiss}
|
|
78
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
79
|
+
>
|
|
80
|
+
<AtomicIcon name="close" size="md" customColor={tokens.colors.textSecondary} />
|
|
81
|
+
</TouchableOpacity>
|
|
82
|
+
)}
|
|
83
|
+
|
|
69
84
|
<View style={styles.content}>
|
|
70
85
|
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
71
86
|
|
|
@@ -93,6 +108,15 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
|
93
108
|
<AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
|
|
94
109
|
{messages.hint}
|
|
95
110
|
</AtomicText>
|
|
111
|
+
|
|
112
|
+
{/* Background hint - tap to dismiss */}
|
|
113
|
+
{onDismiss && (
|
|
114
|
+
<TouchableOpacity style={styles.backgroundHintButton} onPress={onDismiss}>
|
|
115
|
+
<AtomicText type="bodySmall" style={[styles.backgroundHint, { color: tokens.colors.primary }]}>
|
|
116
|
+
{messages.backgroundHint}
|
|
117
|
+
</AtomicText>
|
|
118
|
+
</TouchableOpacity>
|
|
119
|
+
)}
|
|
96
120
|
</View>
|
|
97
121
|
</View>
|
|
98
122
|
);
|
|
@@ -104,6 +128,13 @@ const styles = StyleSheet.create({
|
|
|
104
128
|
justifyContent: "center",
|
|
105
129
|
alignItems: "center",
|
|
106
130
|
},
|
|
131
|
+
closeButton: {
|
|
132
|
+
position: "absolute",
|
|
133
|
+
top: 16,
|
|
134
|
+
right: 16,
|
|
135
|
+
zIndex: 10,
|
|
136
|
+
padding: 8,
|
|
137
|
+
},
|
|
107
138
|
content: {
|
|
108
139
|
width: "80%",
|
|
109
140
|
maxWidth: 400,
|
|
@@ -125,4 +156,13 @@ const styles = StyleSheet.create({
|
|
|
125
156
|
textAlign: "center",
|
|
126
157
|
marginTop: 8,
|
|
127
158
|
},
|
|
159
|
+
backgroundHintButton: {
|
|
160
|
+
marginTop: 24,
|
|
161
|
+
paddingVertical: 12,
|
|
162
|
+
paddingHorizontal: 16,
|
|
163
|
+
},
|
|
164
|
+
backgroundHint: {
|
|
165
|
+
textAlign: "center",
|
|
166
|
+
fontWeight: "600",
|
|
167
|
+
},
|
|
128
168
|
});
|
|
@@ -26,13 +26,28 @@ export async function executeVideoFeature(
|
|
|
26
26
|
request: VideoFeatureRequest,
|
|
27
27
|
options?: ExecuteVideoFeatureOptions,
|
|
28
28
|
): Promise<VideoFeatureResult> {
|
|
29
|
+
if (__DEV__) {
|
|
30
|
+
console.log(`[VideoExecutor:${featureType}] START`, {
|
|
31
|
+
hasSource: !!request.sourceImageBase64,
|
|
32
|
+
hasTarget: !!request.targetImageBase64,
|
|
33
|
+
promptLength: request.prompt?.length ?? 0,
|
|
34
|
+
options: request.options,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
const provider = providerRegistry.getActiveProvider();
|
|
30
39
|
|
|
31
40
|
if (!provider) {
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.log(`[VideoExecutor:${featureType}] ERROR: No provider`);
|
|
43
|
+
}
|
|
32
44
|
return { success: false, error: "No AI provider configured" };
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
if (!provider.isInitialized()) {
|
|
48
|
+
if (__DEV__) {
|
|
49
|
+
console.log(`[VideoExecutor:${featureType}] ERROR: Provider not initialized`);
|
|
50
|
+
}
|
|
36
51
|
return { success: false, error: "AI provider not initialized" };
|
|
37
52
|
}
|
|
38
53
|
|
|
@@ -41,7 +56,7 @@ export async function executeVideoFeature(
|
|
|
41
56
|
const model = provider.getVideoFeatureModel(featureType);
|
|
42
57
|
|
|
43
58
|
if (__DEV__) {
|
|
44
|
-
console.log(`[
|
|
59
|
+
console.log(`[VideoExecutor:${featureType}] Provider: ${provider.providerId}, Model: ${model}`);
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
try {
|
|
@@ -52,31 +67,74 @@ export async function executeVideoFeature(
|
|
|
52
67
|
options: request.options,
|
|
53
68
|
};
|
|
54
69
|
|
|
70
|
+
if (__DEV__) {
|
|
71
|
+
console.log(`[VideoExecutor:${featureType}] InputData prepared`, {
|
|
72
|
+
sourceSize: inputData.sourceImageBase64 ? `${(inputData.sourceImageBase64.length / 1024).toFixed(1)}KB` : "N/A",
|
|
73
|
+
targetSize: inputData.targetImageBase64 ? `${(inputData.targetImageBase64.length / 1024).toFixed(1)}KB` : "N/A",
|
|
74
|
+
prompt: inputData.prompt?.substring(0, 50) + "...",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
const input = provider.buildVideoFeatureInput(featureType, inputData);
|
|
56
79
|
|
|
80
|
+
if (__DEV__) {
|
|
81
|
+
console.log(`[VideoExecutor:${featureType}] Built input for API`, {
|
|
82
|
+
inputKeys: Object.keys(input),
|
|
83
|
+
hasImageUrl: !!(input as Record<string, unknown>).image_url,
|
|
84
|
+
hasPrompt: !!(input as Record<string, unknown>).prompt,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let statusCount = 0;
|
|
57
89
|
const result = await provider.subscribe(model, input, {
|
|
58
90
|
timeoutMs: VIDEO_TIMEOUT_MS,
|
|
59
91
|
onQueueUpdate: (status) => {
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
statusCount++;
|
|
93
|
+
// Log every 10th status update to avoid spam
|
|
94
|
+
if (__DEV__ && statusCount % 10 === 1) {
|
|
95
|
+
console.log(`[VideoExecutor:${featureType}] Queue #${statusCount}:`, status.status);
|
|
62
96
|
}
|
|
63
97
|
onStatusChange?.(status.status);
|
|
64
98
|
},
|
|
65
99
|
});
|
|
66
100
|
|
|
101
|
+
if (__DEV__) {
|
|
102
|
+
console.log(`[VideoExecutor:${featureType}] API Response received`, {
|
|
103
|
+
totalStatusUpdates: statusCount,
|
|
104
|
+
resultKeys: result ? Object.keys(result as object) : "null",
|
|
105
|
+
resultType: typeof result,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
67
109
|
// Check for FAL API error in result (may return with COMPLETED status)
|
|
68
110
|
checkFalApiError(result);
|
|
69
111
|
|
|
70
112
|
const extractor = extractResult ?? extractVideoResult;
|
|
71
113
|
const videoUrl = extractor(result);
|
|
72
114
|
|
|
115
|
+
if (__DEV__) {
|
|
116
|
+
console.log(`[VideoExecutor:${featureType}] Extracted video URL`, {
|
|
117
|
+
hasVideoUrl: !!videoUrl,
|
|
118
|
+
urlPreview: videoUrl ? videoUrl.substring(0, 80) + "..." : "N/A",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
73
122
|
if (!videoUrl) {
|
|
74
123
|
if (__DEV__) {
|
|
75
|
-
console.log(`[
|
|
124
|
+
console.log(`[VideoExecutor:${featureType}] FAILED: No video URL`, {
|
|
125
|
+
result: JSON.stringify(result).substring(0, 500),
|
|
126
|
+
});
|
|
76
127
|
}
|
|
77
128
|
return { success: false, error: "No video in response" };
|
|
78
129
|
}
|
|
79
130
|
|
|
131
|
+
if (__DEV__) {
|
|
132
|
+
console.log(`[VideoExecutor:${featureType}] SUCCESS`, {
|
|
133
|
+
videoUrl: videoUrl.substring(0, 80) + "...",
|
|
134
|
+
requestId: (result as { requestId?: string })?.requestId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
80
138
|
return {
|
|
81
139
|
success: true,
|
|
82
140
|
videoUrl,
|
|
@@ -84,6 +142,12 @@ export async function executeVideoFeature(
|
|
|
84
142
|
};
|
|
85
143
|
} catch (error) {
|
|
86
144
|
const message = extractErrorMessage(error, "Processing failed", `Video:${featureType}`);
|
|
145
|
+
if (__DEV__) {
|
|
146
|
+
console.log(`[VideoExecutor:${featureType}] EXCEPTION`, {
|
|
147
|
+
error: message,
|
|
148
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
87
151
|
return { success: false, error: message };
|
|
88
152
|
}
|
|
89
153
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Note: No Modal wrapper - shows fullscreen progress when processing (FutureUS pattern)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useCallback } from "react";
|
|
8
|
+
import React, { useCallback, useState, useEffect } from "react";
|
|
9
9
|
import { View, ScrollView, StyleSheet } from "react-native";
|
|
10
10
|
import {
|
|
11
11
|
useAppDesignTokens,
|
|
@@ -31,6 +31,19 @@ export const DualImageFeatureLayout: React.FC<DualImageFeatureLayoutProps> = ({
|
|
|
31
31
|
const { width: screenWidth, horizontalPadding } = useResponsive();
|
|
32
32
|
const imageSize = screenWidth - horizontalPadding * 2;
|
|
33
33
|
|
|
34
|
+
// Background generation: user can dismiss progress but generation continues
|
|
35
|
+
const [isProgressDismissed, setIsProgressDismissed] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (feature.isProcessing) {
|
|
39
|
+
setIsProgressDismissed(false);
|
|
40
|
+
}
|
|
41
|
+
}, [feature.isProcessing]);
|
|
42
|
+
|
|
43
|
+
const handleDismissProgress = useCallback(() => {
|
|
44
|
+
setIsProgressDismissed(true);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
34
47
|
const handleProcess = useCallback(() => {
|
|
35
48
|
void feature.process();
|
|
36
49
|
}, [feature]);
|
|
@@ -48,7 +61,8 @@ export const DualImageFeatureLayout: React.FC<DualImageFeatureLayoutProps> = ({
|
|
|
48
61
|
}, [feature]);
|
|
49
62
|
|
|
50
63
|
// Processing view - fullscreen (FutureUS pattern, no Modal)
|
|
51
|
-
if
|
|
64
|
+
// Show only if processing AND not dismissed
|
|
65
|
+
if (feature.isProcessing && !isProgressDismissed) {
|
|
52
66
|
return (
|
|
53
67
|
<View
|
|
54
68
|
style={[
|
|
@@ -63,6 +77,7 @@ export const DualImageFeatureLayout: React.FC<DualImageFeatureLayoutProps> = ({
|
|
|
63
77
|
message={modalTranslations.message}
|
|
64
78
|
hint={modalTranslations.hint}
|
|
65
79
|
backgroundHint={modalTranslations.backgroundHint}
|
|
80
|
+
onClose={handleDismissProgress}
|
|
66
81
|
backgroundColor={tokens.colors.surface}
|
|
67
82
|
textColor={tokens.colors.textPrimary}
|
|
68
83
|
progressColor={tokens.colors.primary}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Note: No Modal wrapper - shows fullscreen progress when processing (FutureUS pattern)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useCallback } from "react";
|
|
8
|
+
import React, { useCallback, useState, useEffect } from "react";
|
|
9
9
|
import { View, ScrollView, StyleSheet } from "react-native";
|
|
10
10
|
import {
|
|
11
11
|
useAppDesignTokens,
|
|
@@ -27,6 +27,19 @@ export const DualImageVideoFeatureLayout: React.FC<DualImageVideoFeatureLayoutPr
|
|
|
27
27
|
}) => {
|
|
28
28
|
const tokens = useAppDesignTokens();
|
|
29
29
|
|
|
30
|
+
// Background generation: user can dismiss progress but generation continues
|
|
31
|
+
const [isProgressDismissed, setIsProgressDismissed] = useState(false);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (feature.isProcessing) {
|
|
35
|
+
setIsProgressDismissed(false);
|
|
36
|
+
}
|
|
37
|
+
}, [feature.isProcessing]);
|
|
38
|
+
|
|
39
|
+
const handleDismissProgress = useCallback(() => {
|
|
40
|
+
setIsProgressDismissed(true);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
30
43
|
const handleProcess = useCallback(() => {
|
|
31
44
|
void feature.process();
|
|
32
45
|
}, [feature]);
|
|
@@ -44,7 +57,8 @@ export const DualImageVideoFeatureLayout: React.FC<DualImageVideoFeatureLayoutPr
|
|
|
44
57
|
}, [feature]);
|
|
45
58
|
|
|
46
59
|
// Processing view - fullscreen (FutureUS pattern, no Modal)
|
|
47
|
-
if
|
|
60
|
+
// Show only if processing AND not dismissed
|
|
61
|
+
if (feature.isProcessing && !isProgressDismissed) {
|
|
48
62
|
return (
|
|
49
63
|
<View
|
|
50
64
|
style={[
|
|
@@ -59,6 +73,7 @@ export const DualImageVideoFeatureLayout: React.FC<DualImageVideoFeatureLayoutPr
|
|
|
59
73
|
message={modalTranslations.message}
|
|
60
74
|
hint={modalTranslations.hint}
|
|
61
75
|
backgroundHint={modalTranslations.backgroundHint}
|
|
76
|
+
onClose={handleDismissProgress}
|
|
62
77
|
backgroundColor={tokens.colors.surface}
|
|
63
78
|
textColor={tokens.colors.textPrimary}
|
|
64
79
|
progressColor={tokens.colors.primary}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Note: No Modal wrapper - shows fullscreen progress when processing (FutureUS pattern)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useCallback } from "react";
|
|
8
|
+
import React, { useCallback, useState, useEffect } from "react";
|
|
9
9
|
import { View, ScrollView, StyleSheet } from "react-native";
|
|
10
10
|
import {
|
|
11
11
|
useAppDesignTokens,
|
|
@@ -32,6 +32,20 @@ export const SingleImageFeatureLayout: React.FC<SingleImageFeatureLayoutProps> =
|
|
|
32
32
|
const { width: screenWidth, horizontalPadding } = useResponsive();
|
|
33
33
|
const imageSize = screenWidth - horizontalPadding * 2;
|
|
34
34
|
|
|
35
|
+
// Background generation: user can dismiss progress but generation continues
|
|
36
|
+
const [isProgressDismissed, setIsProgressDismissed] = useState(false);
|
|
37
|
+
|
|
38
|
+
// Reset dismissed state when processing starts
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (feature.isProcessing) {
|
|
41
|
+
setIsProgressDismissed(false);
|
|
42
|
+
}
|
|
43
|
+
}, [feature.isProcessing]);
|
|
44
|
+
|
|
45
|
+
const handleDismissProgress = useCallback(() => {
|
|
46
|
+
setIsProgressDismissed(true);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
35
49
|
const handleProcess = useCallback(() => {
|
|
36
50
|
void feature.process();
|
|
37
51
|
}, [feature]);
|
|
@@ -45,7 +59,8 @@ export const SingleImageFeatureLayout: React.FC<SingleImageFeatureLayoutProps> =
|
|
|
45
59
|
}, [feature]);
|
|
46
60
|
|
|
47
61
|
// Processing view - fullscreen (FutureUS pattern, no Modal)
|
|
48
|
-
if (
|
|
62
|
+
// Show only if processing AND not dismissed (user can dismiss and generation continues)
|
|
63
|
+
if (feature.isProcessing && !isProgressDismissed) {
|
|
49
64
|
return (
|
|
50
65
|
<View
|
|
51
66
|
style={[
|
|
@@ -60,6 +75,7 @@ export const SingleImageFeatureLayout: React.FC<SingleImageFeatureLayoutProps> =
|
|
|
60
75
|
message={modalTranslations.message}
|
|
61
76
|
hint={modalTranslations.hint}
|
|
62
77
|
backgroundHint={modalTranslations.backgroundHint}
|
|
78
|
+
onClose={handleDismissProgress}
|
|
63
79
|
backgroundColor={tokens.colors.surface}
|
|
64
80
|
textColor={tokens.colors.textPrimary}
|
|
65
81
|
progressColor={tokens.colors.primary}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Note: No Modal wrapper - shows fullscreen progress when processing (FutureUS pattern)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useCallback } from "react";
|
|
9
|
+
import React, { useCallback, useState, useEffect } from "react";
|
|
10
10
|
import { View, ScrollView, StyleSheet } from "react-native";
|
|
11
11
|
import {
|
|
12
12
|
useAppDesignTokens,
|
|
@@ -32,6 +32,19 @@ export const SingleImageWithPromptFeatureLayout: React.FC<SingleImageWithPromptF
|
|
|
32
32
|
const { width: screenWidth, horizontalPadding } = useResponsive();
|
|
33
33
|
const imageSize = screenWidth - horizontalPadding * 2;
|
|
34
34
|
|
|
35
|
+
// Background generation: user can dismiss progress but generation continues
|
|
36
|
+
const [isProgressDismissed, setIsProgressDismissed] = useState(false);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (feature.isProcessing) {
|
|
40
|
+
setIsProgressDismissed(false);
|
|
41
|
+
}
|
|
42
|
+
}, [feature.isProcessing]);
|
|
43
|
+
|
|
44
|
+
const handleDismissProgress = useCallback(() => {
|
|
45
|
+
setIsProgressDismissed(true);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
35
48
|
const handleProcess = useCallback(() => {
|
|
36
49
|
void feature.process();
|
|
37
50
|
}, [feature]);
|
|
@@ -52,7 +65,8 @@ export const SingleImageWithPromptFeatureLayout: React.FC<SingleImageWithPromptF
|
|
|
52
65
|
);
|
|
53
66
|
|
|
54
67
|
// Processing view - fullscreen (FutureUS pattern, no Modal)
|
|
55
|
-
if
|
|
68
|
+
// Show only if processing AND not dismissed
|
|
69
|
+
if (feature.isProcessing && !isProgressDismissed) {
|
|
56
70
|
return (
|
|
57
71
|
<View
|
|
58
72
|
style={[
|
|
@@ -67,6 +81,7 @@ export const SingleImageWithPromptFeatureLayout: React.FC<SingleImageWithPromptF
|
|
|
67
81
|
message={modalTranslations.message}
|
|
68
82
|
hint={modalTranslations.hint}
|
|
69
83
|
backgroundHint={modalTranslations.backgroundHint}
|
|
84
|
+
onClose={handleDismissProgress}
|
|
70
85
|
backgroundColor={tokens.colors.surface}
|
|
71
86
|
textColor={tokens.colors.textPrimary}
|
|
72
87
|
progressColor={tokens.colors.primary}
|