@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.33.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 => {
@@ -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
- return photosBase64.filter(Boolean) as string[];
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 for", scenario.id);
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
- return {
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 progress={generationProgress} scenario={scenario} t={t} />
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
- readonly onCancel?: () => void;
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
- onCancel: _onCancel,
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(`[Video:${featureType}] Provider: ${provider.providerId}, Model: ${model}`);
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
- if (__DEV__) {
61
- console.log(`[Video:${featureType}] Queue status:`, status.status);
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(`[Video:${featureType}] No video URL found in result`);
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 (feature.isProcessing) {
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 (feature.isProcessing) {
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 (feature.isProcessing) {
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 (feature.isProcessing) {
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}