@umituz/react-native-ai-generation-content 1.17.182 → 1.17.184

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.
Files changed (20) hide show
  1. package/package.json +1 -1
  2. package/src/features/ai-hug/presentation/components/AIHugFeature.tsx +40 -111
  3. package/src/features/ai-kiss/presentation/components/AIKissFeature.tsx +40 -105
  4. package/src/features/anime-selfie/presentation/components/AnimeSelfieFeature.tsx +48 -121
  5. package/src/features/face-swap/presentation/components/FaceSwapFeature.tsx +48 -121
  6. package/src/features/hd-touch-up/presentation/components/HDTouchUpFeature.tsx +48 -120
  7. package/src/features/image-to-image/domain/types/base.types.ts +16 -0
  8. package/src/features/photo-restoration/presentation/components/PhotoRestoreFeature.tsx +49 -110
  9. package/src/features/remove-background/presentation/components/RemoveBackgroundFeature.tsx +48 -122
  10. package/src/features/remove-object/presentation/components/RemoveObjectFeature.tsx +47 -117
  11. package/src/features/replace-background/presentation/components/ReplaceBackgroundFeature.tsx +48 -121
  12. package/src/features/upscaling/presentation/components/UpscaleFeature.tsx +50 -120
  13. package/src/index.ts +13 -0
  14. package/src/infrastructure/services/image-feature-executor.service.ts +18 -3
  15. package/src/presentation/layouts/DualImageFeatureLayout.tsx +149 -0
  16. package/src/presentation/layouts/DualImageVideoFeatureLayout.tsx +143 -0
  17. package/src/presentation/layouts/SingleImageFeatureLayout.tsx +163 -0
  18. package/src/presentation/layouts/SingleImageWithPromptFeatureLayout.tsx +153 -0
  19. package/src/presentation/layouts/index.ts +26 -0
  20. package/src/presentation/layouts/types.ts +228 -0
@@ -1,17 +1,13 @@
1
1
  /**
2
2
  * UpscaleFeature Component
3
3
  * Self-contained upscale feature UI component
4
- * Uses hook internally, only requires config and translations
4
+ * Uses centralized SingleImageFeatureLayout for consistent UX
5
5
  */
6
6
 
7
- import React, { useCallback, useMemo } from "react";
8
- import { ScrollView, StyleSheet } from "react-native";
9
- import {
10
- useAppDesignTokens,
11
- AtomicText,
12
- } from "@umituz/react-native-design-system";
7
+ import React, { useMemo } from "react";
13
8
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
14
- import { AIGenerationForm } from "../../../../presentation/components/AIGenerationForm";
9
+ import { SingleImageFeatureLayout } from "../../../../presentation/layouts";
10
+ import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
15
11
  import { UpscaleResultView } from "./UpscaleResultView";
16
12
  import { useUpscaleFeature } from "../hooks";
17
13
  import type {
@@ -20,21 +16,17 @@ import type {
20
16
  } from "../../domain/types";
21
17
 
22
18
  export interface UpscaleFeatureProps {
23
- /** Feature configuration with provider-specific settings */
24
19
  config: UpscaleFeatureConfig;
25
- /** Translations for all UI text */
26
- translations: UpscaleTranslations;
27
- /** Image picker callback */
20
+ translations: UpscaleTranslations & {
21
+ modalTitle?: string;
22
+ modalMessage?: string;
23
+ modalHint?: string;
24
+ modalBackgroundHint?: string;
25
+ };
28
26
  onSelectImage: () => Promise<string | null>;
29
- /** Save image callback */
30
27
  onSaveImage: (imageUrl: string) => Promise<void>;
31
- /** Called before processing starts. Return false to cancel. */
32
28
  onBeforeProcess?: () => Promise<boolean>;
33
- /** Optional custom processing modal renderer */
34
- renderProcessingModal?: (props: {
35
- visible: boolean;
36
- progress: number;
37
- }) => React.ReactNode;
29
+ renderProcessingModal?: (props: ProcessingModalRenderProps) => React.ReactNode;
38
30
  }
39
31
 
40
32
  export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
@@ -45,8 +37,6 @@ export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
45
37
  onBeforeProcess,
46
38
  renderProcessingModal,
47
39
  }) => {
48
- const tokens = useAppDesignTokens();
49
-
50
40
  const feature = useUpscaleFeature({
51
41
  config,
52
42
  onSelectImage,
@@ -54,38 +44,47 @@ export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
54
44
  onBeforeProcess,
55
45
  });
56
46
 
57
- const photoTranslations = useMemo(
47
+ const modalTranslations = useMemo(
58
48
  () => ({
59
- tapToUpload: translations.uploadTitle,
60
- selectPhoto: translations.uploadSubtitle,
61
- change: translations.uploadChange,
62
- analyzing: translations.uploadAnalyzing,
49
+ title: translations.modalTitle || "Processing",
50
+ message: translations.modalMessage || "AI is upscaling your image...",
51
+ hint: translations.modalHint || "This may take a moment",
52
+ backgroundHint: translations.modalBackgroundHint || "Continue in background",
63
53
  }),
64
54
  [translations],
65
55
  );
66
56
 
67
- const handleProcess = useCallback(() => {
68
- void feature.process();
69
- }, [feature]);
70
-
71
- const handleSave = useCallback(() => {
72
- void feature.save();
73
- }, [feature]);
74
-
75
- const handleSelectImage = useCallback(() => {
76
- void feature.selectImage();
77
- }, [feature]);
78
-
79
- if (feature.processedUrl && feature.imageUri) {
80
- return (
81
- <ScrollView
82
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
83
- contentContainerStyle={styles.content}
84
- showsVerticalScrollIndicator={false}
85
- >
57
+ return (
58
+ <SingleImageFeatureLayout
59
+ feature={feature}
60
+ translations={translations}
61
+ modalTranslations={modalTranslations}
62
+ description={translations.description}
63
+ renderProcessingModal={renderProcessingModal}
64
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing }) => (
65
+ <PhotoUploadCard
66
+ imageUri={imageUri}
67
+ onPress={onSelect}
68
+ isValidating={isProcessing}
69
+ disabled={isDisabled}
70
+ translations={{
71
+ tapToUpload: translations.uploadTitle,
72
+ selectPhoto: translations.uploadSubtitle,
73
+ change: translations.uploadChange,
74
+ analyzing: translations.uploadAnalyzing,
75
+ }}
76
+ config={{
77
+ aspectRatio: 1,
78
+ borderRadius: 24,
79
+ showValidationStatus: false,
80
+ allowChange: true,
81
+ }}
82
+ />
83
+ )}
84
+ renderCustomResult={({ processedUrl, originalImageUri, onSave, onReset }) => (
86
85
  <UpscaleResultView
87
- originalUri={feature.imageUri}
88
- processedUri={feature.processedUrl}
86
+ originalUri={originalImageUri}
87
+ processedUri={processedUrl}
89
88
  translations={{
90
89
  successText: translations.successText,
91
90
  saveButtonText: translations.saveButtonText,
@@ -93,79 +92,10 @@ export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
93
92
  beforeLabel: translations.beforeLabel,
94
93
  afterLabel: translations.afterLabel,
95
94
  }}
96
- onSave={handleSave}
97
- onReset={feature.reset}
95
+ onSave={onSave}
96
+ onReset={onReset}
98
97
  />
99
- </ScrollView>
100
- );
101
- }
102
-
103
- return (
104
- <>
105
- <ScrollView
106
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
107
- contentContainerStyle={styles.content}
108
- showsVerticalScrollIndicator={false}
109
- >
110
- <AIGenerationForm
111
- onGenerate={handleProcess}
112
- isGenerating={feature.isProcessing}
113
- progress={feature.progress}
114
- translations={{
115
- generateButton: translations.processButtonText,
116
- generatingButton: translations.processingText,
117
- progressTitle: translations.processingText,
118
- }}
119
- >
120
- <AtomicText
121
- type="bodyLarge"
122
- style={[styles.description, { color: tokens.colors.textSecondary }]}
123
- >
124
- {translations.description}
125
- </AtomicText>
126
-
127
- <PhotoUploadCard
128
- imageUri={feature.imageUri}
129
- onPress={handleSelectImage}
130
- isValidating={feature.isProcessing}
131
- disabled={feature.isProcessing}
132
- translations={photoTranslations}
133
- config={{
134
- aspectRatio: 1,
135
- borderRadius: 24,
136
- showValidationStatus: false,
137
- allowChange: true,
138
- }}
139
- />
140
- </AIGenerationForm>
141
- </ScrollView>
142
-
143
- {renderProcessingModal?.({ visible: feature.isProcessing, progress: feature.progress })}
144
- </>
98
+ )}
99
+ />
145
100
  );
146
101
  };
147
-
148
- const styles = StyleSheet.create({
149
- container: {
150
- flex: 1,
151
- },
152
- content: {
153
- paddingVertical: 16,
154
- },
155
- description: {
156
- textAlign: "center",
157
- marginHorizontal: 24,
158
- marginBottom: 24,
159
- lineHeight: 24,
160
- },
161
- errorContainer: {
162
- marginHorizontal: 24,
163
- marginBottom: 16,
164
- padding: 16,
165
- borderRadius: 12,
166
- },
167
- buttonContainer: {
168
- marginHorizontal: 24,
169
- marginTop: 8,
170
- },
171
- });
package/src/index.ts CHANGED
@@ -92,6 +92,19 @@ export {
92
92
  ASPECT_RATIO_IDS, COMMON_DURATIONS,
93
93
  } from "./presentation/components";
94
94
 
95
+ export {
96
+ SingleImageFeatureLayout, SingleImageWithPromptFeatureLayout,
97
+ DualImageFeatureLayout, DualImageVideoFeatureLayout,
98
+ } from "./presentation/layouts";
99
+ export type {
100
+ ModalTranslations, BaseLayoutTranslations, PhotoUploadTranslations,
101
+ SingleImageInputRenderProps, SingleImageWithPromptInputRenderProps,
102
+ SingleImageWithPromptFeatureState, SingleImageWithPromptFeatureLayoutProps,
103
+ DualImageInputRenderProps, ResultRenderProps, CustomResultRenderProps,
104
+ ProcessingModalRenderProps, SingleImageFeatureLayoutProps, DualImageFeatureLayoutProps,
105
+ DualImageVideoFeatureState, DualImageVideoFeatureLayoutProps,
106
+ } from "./presentation/layouts";
107
+
95
108
  export type {
96
109
  GenerationProgressModalProps, GenerationProgressRenderProps, GenerationProgressContentProps,
97
110
  GenerationProgressBarProps, PendingJobCardProps, StatusLabels, PendingJobProgressBarProps,
@@ -158,10 +158,25 @@ export async function executeImageFeature(
158
158
  requestId: (result as { requestId?: string })?.requestId,
159
159
  };
160
160
  } catch (error) {
161
- const message = error instanceof Error ? error.message : String(error);
161
+ // Extract detailed error message from FAL API errors
162
+ let message = "Processing failed";
163
+ if (error instanceof Error) {
164
+ message = error.message;
165
+ } else if (typeof error === "object" && error !== null) {
166
+ const errObj = error as Record<string, unknown>;
167
+ // FAL API error format: {detail: [{msg, type, loc}]} or {message}
168
+ if (Array.isArray(errObj.detail) && errObj.detail[0]?.msg) {
169
+ message = String(errObj.detail[0].msg);
170
+ } else if (errObj.detail) {
171
+ message = JSON.stringify(errObj.detail);
172
+ } else if (errObj.message) {
173
+ message = String(errObj.message);
174
+ } else if (errObj.msg) {
175
+ message = String(errObj.msg);
176
+ }
177
+ }
162
178
  if (__DEV__) {
163
-
164
- console.error(`[Image:${featureType}] Error:`, message);
179
+ console.error(`[Image:${featureType}] Error:`, message, error);
165
180
  }
166
181
  return { success: false, error: message };
167
182
  }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * DualImageFeatureLayout
3
+ * Centralized layout for all dual-image processing features (face-swap, ai-kiss, ai-hug)
4
+ * Handles: Modal, ScrollView, AIGenerationForm, AIGenerationResult
5
+ */
6
+
7
+ import React, { useCallback } from "react";
8
+ import { ScrollView, StyleSheet } from "react-native";
9
+ import {
10
+ useAppDesignTokens,
11
+ useResponsive,
12
+ AtomicText,
13
+ } from "@umituz/react-native-design-system";
14
+ import { AIGenerationForm } from "../components/AIGenerationForm";
15
+ import { AIGenerationResult } from "../components/display/AIGenerationResult";
16
+ import { GenerationProgressModal } from "../components/GenerationProgressModal";
17
+ import type { DualImageFeatureLayoutProps } from "./types";
18
+
19
+ export const DualImageFeatureLayout: React.FC<DualImageFeatureLayoutProps> = ({
20
+ feature,
21
+ translations,
22
+ modalTranslations,
23
+ modalIcon = "sparkles",
24
+ renderInput,
25
+ renderResult,
26
+ description,
27
+ renderProcessingModal,
28
+ children,
29
+ }) => {
30
+ const tokens = useAppDesignTokens();
31
+ const { width: screenWidth, horizontalPadding } = useResponsive();
32
+ const imageSize = screenWidth - horizontalPadding * 2;
33
+
34
+ const handleProcess = useCallback(() => {
35
+ void feature.process();
36
+ }, [feature]);
37
+
38
+ const handleSave = useCallback(() => {
39
+ void feature.save();
40
+ }, [feature]);
41
+
42
+ const handleSelectSource = useCallback(() => {
43
+ void feature.selectSourceImage();
44
+ }, [feature]);
45
+
46
+ const handleSelectTarget = useCallback(() => {
47
+ void feature.selectTargetImage();
48
+ }, [feature]);
49
+
50
+ // Default modal
51
+ const defaultModal = (
52
+ <GenerationProgressModal
53
+ visible={feature.isProcessing}
54
+ progress={feature.progress}
55
+ icon={modalIcon}
56
+ title={modalTranslations.title}
57
+ message={modalTranslations.message}
58
+ hint={modalTranslations.hint}
59
+ backgroundHint={modalTranslations.backgroundHint}
60
+ onClose={() => {}}
61
+ />
62
+ );
63
+
64
+ // Result view
65
+ if (feature.processedUrl) {
66
+ return (
67
+ <ScrollView
68
+ style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
69
+ contentContainerStyle={styles.content}
70
+ showsVerticalScrollIndicator={false}
71
+ >
72
+ <AIGenerationResult
73
+ successText={translations.successText}
74
+ primaryAction={{
75
+ label: translations.saveButtonText,
76
+ onPress: handleSave,
77
+ }}
78
+ secondaryAction={{
79
+ label: translations.tryAnotherText,
80
+ onPress: feature.reset,
81
+ }}
82
+ >
83
+ {renderResult({ imageUrl: feature.processedUrl, imageSize })}
84
+ </AIGenerationResult>
85
+ </ScrollView>
86
+ );
87
+ }
88
+
89
+ // Input view
90
+ return (
91
+ <>
92
+ <ScrollView
93
+ style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
94
+ contentContainerStyle={styles.content}
95
+ showsVerticalScrollIndicator={false}
96
+ >
97
+ <AIGenerationForm
98
+ onGenerate={handleProcess}
99
+ isGenerating={feature.isProcessing}
100
+ progress={feature.progress}
101
+ translations={{
102
+ generateButton: translations.processButtonText,
103
+ generatingButton: translations.processingText,
104
+ progressTitle: translations.processingText,
105
+ }}
106
+ >
107
+ {description && (
108
+ <AtomicText
109
+ type="bodyLarge"
110
+ style={[styles.description, { color: tokens.colors.textSecondary }]}
111
+ >
112
+ {description}
113
+ </AtomicText>
114
+ )}
115
+
116
+ {children}
117
+
118
+ {renderInput({
119
+ sourceImageUri: feature.sourceImageUri,
120
+ targetImageUri: feature.targetImageUri,
121
+ onSelectSource: handleSelectSource,
122
+ onSelectTarget: handleSelectTarget,
123
+ isDisabled: feature.isProcessing,
124
+ isProcessing: feature.isProcessing,
125
+ })}
126
+ </AIGenerationForm>
127
+ </ScrollView>
128
+
129
+ {renderProcessingModal
130
+ ? renderProcessingModal({ visible: feature.isProcessing, progress: feature.progress })
131
+ : defaultModal}
132
+ </>
133
+ );
134
+ };
135
+
136
+ const styles = StyleSheet.create({
137
+ container: {
138
+ flex: 1,
139
+ },
140
+ content: {
141
+ paddingVertical: 16,
142
+ },
143
+ description: {
144
+ textAlign: "center",
145
+ marginHorizontal: 24,
146
+ marginBottom: 24,
147
+ lineHeight: 24,
148
+ },
149
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * DualImageVideoFeatureLayout
3
+ * Centralized layout for dual-image video features (ai-kiss, ai-hug)
4
+ * Handles: Modal, ScrollView, AIGenerationForm, AIGenerationResult
5
+ */
6
+
7
+ import React, { useCallback } from "react";
8
+ import { ScrollView, StyleSheet } from "react-native";
9
+ import {
10
+ useAppDesignTokens,
11
+ AtomicText,
12
+ } from "@umituz/react-native-design-system";
13
+ import { AIGenerationForm } from "../components/AIGenerationForm";
14
+ import { AIGenerationResult } from "../components/display/AIGenerationResult";
15
+ import { GenerationProgressModal } from "../components/GenerationProgressModal";
16
+ import type { DualImageVideoFeatureLayoutProps } from "./types";
17
+
18
+ export const DualImageVideoFeatureLayout: React.FC<DualImageVideoFeatureLayoutProps> = ({
19
+ feature,
20
+ translations,
21
+ modalTranslations,
22
+ modalIcon = "sparkles",
23
+ renderInput,
24
+ description,
25
+ renderProcessingModal,
26
+ children,
27
+ }) => {
28
+ const tokens = useAppDesignTokens();
29
+
30
+ const handleProcess = useCallback(() => {
31
+ void feature.process();
32
+ }, [feature]);
33
+
34
+ const handleSave = useCallback(() => {
35
+ void feature.save();
36
+ }, [feature]);
37
+
38
+ const handleSelectSource = useCallback(() => {
39
+ void feature.selectSourceImage();
40
+ }, [feature]);
41
+
42
+ const handleSelectTarget = useCallback(() => {
43
+ void feature.selectTargetImage();
44
+ }, [feature]);
45
+
46
+ // Default modal
47
+ const defaultModal = (
48
+ <GenerationProgressModal
49
+ visible={feature.isProcessing}
50
+ progress={feature.progress}
51
+ icon={modalIcon}
52
+ title={modalTranslations.title}
53
+ message={modalTranslations.message}
54
+ hint={modalTranslations.hint}
55
+ backgroundHint={modalTranslations.backgroundHint}
56
+ onClose={() => {}}
57
+ />
58
+ );
59
+
60
+ // Result view (video features show result without embedded content)
61
+ if (feature.processedVideoUrl) {
62
+ return (
63
+ <ScrollView
64
+ style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
65
+ contentContainerStyle={styles.content}
66
+ showsVerticalScrollIndicator={false}
67
+ >
68
+ <AIGenerationResult
69
+ successText={translations.successText}
70
+ primaryAction={{
71
+ label: translations.saveButtonText,
72
+ onPress: handleSave,
73
+ }}
74
+ secondaryAction={{
75
+ label: translations.tryAnotherText,
76
+ onPress: feature.reset,
77
+ }}
78
+ />
79
+ </ScrollView>
80
+ );
81
+ }
82
+
83
+ // Input view
84
+ return (
85
+ <>
86
+ <ScrollView
87
+ style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
88
+ contentContainerStyle={styles.content}
89
+ showsVerticalScrollIndicator={false}
90
+ >
91
+ <AIGenerationForm
92
+ onGenerate={handleProcess}
93
+ isGenerating={feature.isProcessing}
94
+ progress={feature.progress}
95
+ translations={{
96
+ generateButton: translations.processButtonText,
97
+ generatingButton: translations.processingText,
98
+ progressTitle: translations.processingText,
99
+ }}
100
+ >
101
+ {description && (
102
+ <AtomicText
103
+ type="bodyLarge"
104
+ style={[styles.description, { color: tokens.colors.textSecondary }]}
105
+ >
106
+ {description}
107
+ </AtomicText>
108
+ )}
109
+
110
+ {children}
111
+
112
+ {renderInput({
113
+ sourceImageUri: feature.sourceImageUri,
114
+ targetImageUri: feature.targetImageUri,
115
+ onSelectSource: handleSelectSource,
116
+ onSelectTarget: handleSelectTarget,
117
+ isDisabled: feature.isProcessing,
118
+ isProcessing: feature.isProcessing,
119
+ })}
120
+ </AIGenerationForm>
121
+ </ScrollView>
122
+
123
+ {renderProcessingModal
124
+ ? renderProcessingModal({ visible: feature.isProcessing, progress: feature.progress })
125
+ : defaultModal}
126
+ </>
127
+ );
128
+ };
129
+
130
+ const styles = StyleSheet.create({
131
+ container: {
132
+ flex: 1,
133
+ },
134
+ content: {
135
+ paddingVertical: 16,
136
+ },
137
+ description: {
138
+ textAlign: "center",
139
+ marginHorizontal: 24,
140
+ marginBottom: 24,
141
+ lineHeight: 24,
142
+ },
143
+ });