@umituz/react-native-ai-generation-content 1.26.40 → 1.26.41
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/wizard/presentation/components/GenericWizardFlow.tsx +40 -82
- package/src/domains/generation/wizard/presentation/hooks/usePhotoUploadState.ts +3 -1
- package/src/domains/generation/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +229 -0
- package/src/domains/generation/wizard/presentation/screens/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.41",
|
|
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",
|
|
@@ -21,8 +21,8 @@ import type { WizardFeatureConfig, WizardStepConfig } from "../../domain/entitie
|
|
|
21
21
|
import { buildFlowStepsFromWizard } from "../../infrastructure/builders/dynamic-step-builder";
|
|
22
22
|
import { useWizardGeneration, type WizardScenarioData } from "../hooks/useWizardGeneration";
|
|
23
23
|
import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
|
|
25
|
+
import { GenericPhotoUploadScreen } from "../screens/GenericPhotoUploadScreen";
|
|
26
26
|
|
|
27
27
|
export interface GenericWizardFlowProps {
|
|
28
28
|
readonly featureConfig: WizardFeatureConfig;
|
|
@@ -54,7 +54,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
54
54
|
onCreditsExhausted,
|
|
55
55
|
onBack,
|
|
56
56
|
t,
|
|
57
|
-
translations,
|
|
57
|
+
translations: _translations,
|
|
58
58
|
renderPreview,
|
|
59
59
|
renderGenerating,
|
|
60
60
|
renderResult,
|
|
@@ -187,44 +187,6 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
187
187
|
}
|
|
188
188
|
}, [currentStep, currentStepIndex, onStepChange]);
|
|
189
189
|
|
|
190
|
-
// Handle step continue
|
|
191
|
-
const handleStepContinue = useCallback(
|
|
192
|
-
(stepData: Record<string, unknown>) => {
|
|
193
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
194
|
-
console.log("[GenericWizardFlow] Step continue", {
|
|
195
|
-
stepId: currentStep?.id,
|
|
196
|
-
data: stepData,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Store step data in custom data
|
|
201
|
-
Object.entries(stepData).forEach(([key, value]) => {
|
|
202
|
-
setCustomData(key, value);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Check if this is the last step before generating
|
|
206
|
-
if (currentStepIndex === flowSteps.length - 2) {
|
|
207
|
-
// Next step is GENERATING
|
|
208
|
-
// Notify parent and provide callback to proceed to generating
|
|
209
|
-
// Parent will call proceedToGenerating() after feature gate passes
|
|
210
|
-
if (onGenerationStart) {
|
|
211
|
-
onGenerationStart(customData, () => {
|
|
212
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
213
|
-
console.log("[GenericWizardFlow] Proceeding to GENERATING step");
|
|
214
|
-
}
|
|
215
|
-
nextStep();
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
// DON'T call nextStep() here - parent will call it via proceedToGenerating callback
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Move to next step (for all non-generation steps)
|
|
223
|
-
nextStep();
|
|
224
|
-
},
|
|
225
|
-
[currentStep, currentStepIndex, customData, setCustomData, nextStep, flowSteps.length, onGenerationStart],
|
|
226
|
-
);
|
|
227
|
-
|
|
228
190
|
// Handle back
|
|
229
191
|
const handleBack = useCallback(() => {
|
|
230
192
|
if (currentStepIndex === 0) {
|
|
@@ -234,24 +196,26 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
234
196
|
}
|
|
235
197
|
}, [currentStepIndex, previousStep, onBack]);
|
|
236
198
|
|
|
237
|
-
//
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
199
|
+
// Handle photo continue - saves photo and moves to next step
|
|
200
|
+
const handlePhotoContinue = useCallback((stepId: string, image: UploadedImage) => {
|
|
201
|
+
setCustomData(stepId, image);
|
|
202
|
+
|
|
203
|
+
// Check if this is the last step before generating
|
|
204
|
+
if (currentStepIndex === flowSteps.length - 2) {
|
|
205
|
+
// Next step is GENERATING - call onGenerationStart
|
|
206
|
+
if (onGenerationStart) {
|
|
207
|
+
onGenerationStart({ ...customData, [stepId]: image }, () => {
|
|
208
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
209
|
+
console.log("[GenericWizardFlow] Proceeding to GENERATING step");
|
|
210
|
+
}
|
|
211
|
+
nextStep();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
253
215
|
}
|
|
254
|
-
|
|
216
|
+
|
|
217
|
+
nextStep();
|
|
218
|
+
}, [currentStepIndex, flowSteps.length, customData, setCustomData, nextStep, onGenerationStart]);
|
|
255
219
|
|
|
256
220
|
// Render current step
|
|
257
221
|
const renderCurrentStep = useCallback(() => {
|
|
@@ -290,37 +254,31 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
290
254
|
const titleKey = wizardConfig?.titleKey || `wizard.steps.${step.id}.title`;
|
|
291
255
|
const title = t(titleKey);
|
|
292
256
|
|
|
293
|
-
// Subtitle
|
|
294
|
-
const
|
|
257
|
+
// Subtitle from config
|
|
258
|
+
const subtitleKey = wizardConfig?.subtitleKey || `wizard.steps.${step.id}.subtitle`;
|
|
259
|
+
const subtitle = t(subtitleKey);
|
|
295
260
|
|
|
296
261
|
// Get existing photo for this step from customData
|
|
297
|
-
const existingPhoto = customData[step.id];
|
|
298
|
-
const imageUri = existingPhoto && typeof existingPhoto === "object" && "uri" in existingPhoto
|
|
299
|
-
? (existingPhoto.uri as string)
|
|
300
|
-
: photoUploadHook.image?.uri || null;
|
|
262
|
+
const existingPhoto = customData[step.id] as UploadedImage | undefined;
|
|
301
263
|
|
|
302
264
|
return (
|
|
303
|
-
<
|
|
304
|
-
config={{
|
|
305
|
-
enabled: true,
|
|
306
|
-
order: currentStepIndex,
|
|
307
|
-
id: step.id,
|
|
308
|
-
header: {},
|
|
309
|
-
photoCard: {},
|
|
310
|
-
enableValidation: false,
|
|
311
|
-
}}
|
|
312
|
-
imageUri={imageUri}
|
|
313
|
-
isValidating={false}
|
|
314
|
-
isValid={null}
|
|
315
|
-
onPhotoSelect={photoUploadHook.handlePickImage}
|
|
316
|
-
disabled={false}
|
|
317
|
-
title={title}
|
|
318
|
-
subtitle={subtitle}
|
|
265
|
+
<GenericPhotoUploadScreen
|
|
319
266
|
translations={{
|
|
267
|
+
title,
|
|
268
|
+
subtitle,
|
|
269
|
+
continue: t("common.continue"),
|
|
320
270
|
tapToUpload: t("photoUpload.tapToUpload"),
|
|
321
271
|
selectPhoto: t("photoUpload.selectPhoto"),
|
|
322
272
|
change: t("common.change"),
|
|
273
|
+
fileTooLarge: t("common.errors.file_too_large"),
|
|
274
|
+
maxFileSize: t("common.errors.max_file_size"),
|
|
275
|
+
error: t("common.error"),
|
|
276
|
+
uploadFailed: t("common.errors.upload_failed"),
|
|
323
277
|
}}
|
|
278
|
+
t={t}
|
|
279
|
+
onBack={handleBack}
|
|
280
|
+
onContinue={(image) => handlePhotoContinue(step.id, image)}
|
|
281
|
+
existingImage={existingPhoto}
|
|
324
282
|
/>
|
|
325
283
|
);
|
|
326
284
|
}
|
|
@@ -334,16 +292,16 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
334
292
|
}
|
|
335
293
|
}, [
|
|
336
294
|
currentStep,
|
|
295
|
+
customData,
|
|
337
296
|
generationProgress,
|
|
338
297
|
generationResult,
|
|
339
298
|
nextStep,
|
|
340
299
|
renderPreview,
|
|
341
300
|
renderGenerating,
|
|
342
301
|
renderResult,
|
|
343
|
-
|
|
302
|
+
handlePhotoContinue,
|
|
344
303
|
handleBack,
|
|
345
304
|
t,
|
|
346
|
-
translations,
|
|
347
305
|
]);
|
|
348
306
|
|
|
349
307
|
return (
|
|
@@ -23,6 +23,7 @@ export interface PhotoUploadTranslations {
|
|
|
23
23
|
export interface UsePhotoUploadStateProps {
|
|
24
24
|
readonly config?: PhotoUploadConfig;
|
|
25
25
|
readonly translations: PhotoUploadTranslations;
|
|
26
|
+
readonly initialImage?: UploadedImage;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface UsePhotoUploadStateReturn {
|
|
@@ -36,8 +37,9 @@ const DEFAULT_MAX_FILE_SIZE_MB = 10;
|
|
|
36
37
|
export const usePhotoUploadState = ({
|
|
37
38
|
config,
|
|
38
39
|
translations,
|
|
40
|
+
initialImage,
|
|
39
41
|
}: UsePhotoUploadStateProps): UsePhotoUploadStateReturn => {
|
|
40
|
-
const [image, setImage] = useState<UploadedImage | null>(null);
|
|
42
|
+
const [image, setImage] = useState<UploadedImage | null>(initialImage || null);
|
|
41
43
|
|
|
42
44
|
const maxFileSizeMB = config?.maxFileSizeMB ?? DEFAULT_MAX_FILE_SIZE_MB;
|
|
43
45
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Photo Upload Screen
|
|
3
|
+
* Used by wizard domain for ANY photo upload step
|
|
4
|
+
* NO feature-specific concepts (no partner, couple, etc.)
|
|
5
|
+
* Works for: couple features, face-swap, image-to-video, ANY photo upload need
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from "react";
|
|
9
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
10
|
+
import {
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
ScreenLayout,
|
|
13
|
+
AtomicText,
|
|
14
|
+
AtomicIcon,
|
|
15
|
+
NavigationHeader,
|
|
16
|
+
InfoGrid,
|
|
17
|
+
type DesignTokens,
|
|
18
|
+
type InfoGridItem,
|
|
19
|
+
} from "@umituz/react-native-design-system";
|
|
20
|
+
import { PhotoUploadCard } from "../../../../../presentation/components";
|
|
21
|
+
import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
|
|
22
|
+
import { usePhotoUploadState } from "../hooks/usePhotoUploadState";
|
|
23
|
+
|
|
24
|
+
export interface PhotoUploadScreenTranslations {
|
|
25
|
+
readonly title: string;
|
|
26
|
+
readonly subtitle: string;
|
|
27
|
+
readonly continue: string;
|
|
28
|
+
readonly tapToUpload: string;
|
|
29
|
+
readonly selectPhoto: string;
|
|
30
|
+
readonly change: string;
|
|
31
|
+
readonly analyzing?: string;
|
|
32
|
+
readonly fileTooLarge: string;
|
|
33
|
+
readonly maxFileSize: string;
|
|
34
|
+
readonly error: string;
|
|
35
|
+
readonly uploadFailed: string;
|
|
36
|
+
readonly aiDisclosure?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PhotoUploadScreenConfig {
|
|
40
|
+
readonly showPhotoTips?: boolean;
|
|
41
|
+
readonly maxFileSizeMB?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PhotoUploadScreenProps {
|
|
45
|
+
readonly translations: PhotoUploadScreenTranslations;
|
|
46
|
+
readonly t: (key: string) => string;
|
|
47
|
+
readonly config?: PhotoUploadScreenConfig;
|
|
48
|
+
readonly onBack: () => void;
|
|
49
|
+
readonly onContinue: (image: UploadedImage) => void;
|
|
50
|
+
readonly existingImage?: UploadedImage | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULT_CONFIG: PhotoUploadScreenConfig = {
|
|
54
|
+
showPhotoTips: true,
|
|
55
|
+
maxFileSizeMB: 10,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
|
|
59
|
+
translations,
|
|
60
|
+
t,
|
|
61
|
+
config = DEFAULT_CONFIG,
|
|
62
|
+
onBack,
|
|
63
|
+
onContinue,
|
|
64
|
+
existingImage,
|
|
65
|
+
}) => {
|
|
66
|
+
const tokens = useAppDesignTokens();
|
|
67
|
+
|
|
68
|
+
const { image, handlePickImage, canContinue } = usePhotoUploadState({
|
|
69
|
+
config: { maxFileSizeMB: config.maxFileSizeMB },
|
|
70
|
+
translations: {
|
|
71
|
+
fileTooLarge: translations.fileTooLarge,
|
|
72
|
+
maxFileSize: translations.maxFileSize,
|
|
73
|
+
error: translations.error,
|
|
74
|
+
uploadFailed: translations.uploadFailed,
|
|
75
|
+
},
|
|
76
|
+
initialImage: existingImage || undefined,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const handleContinuePress = () => {
|
|
80
|
+
if (!canContinue || !image) return;
|
|
81
|
+
onContinue(image);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
85
|
+
const showPhotoTips = config.showPhotoTips ?? true;
|
|
86
|
+
|
|
87
|
+
// Build photo tips items from translations
|
|
88
|
+
const photoTipsItems: InfoGridItem[] = useMemo(() => {
|
|
89
|
+
const tipKeys = [
|
|
90
|
+
{ key: "photoUpload.tips.clearFace", icon: "Smile" },
|
|
91
|
+
{ key: "photoUpload.tips.goodLighting", icon: "Sun" },
|
|
92
|
+
{ key: "photoUpload.tips.recentPhoto", icon: "Clock" },
|
|
93
|
+
{ key: "photoUpload.tips.noFilters", icon: "Image" },
|
|
94
|
+
];
|
|
95
|
+
return tipKeys.map(({ key, icon }) => ({
|
|
96
|
+
text: t(key),
|
|
97
|
+
icon,
|
|
98
|
+
}));
|
|
99
|
+
}, [t]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
103
|
+
<NavigationHeader
|
|
104
|
+
title={translations.title}
|
|
105
|
+
onBackPress={onBack}
|
|
106
|
+
rightElement={
|
|
107
|
+
<TouchableOpacity
|
|
108
|
+
onPress={handleContinuePress}
|
|
109
|
+
activeOpacity={0.7}
|
|
110
|
+
disabled={!canContinue || !image}
|
|
111
|
+
style={[
|
|
112
|
+
styles.continueButton,
|
|
113
|
+
{
|
|
114
|
+
backgroundColor: canContinue && image ? tokens.colors.primary : tokens.colors.surfaceVariant,
|
|
115
|
+
opacity: canContinue && image ? 1 : 0.5,
|
|
116
|
+
},
|
|
117
|
+
]}
|
|
118
|
+
>
|
|
119
|
+
<AtomicText
|
|
120
|
+
type="bodyMedium"
|
|
121
|
+
style={[
|
|
122
|
+
styles.continueText,
|
|
123
|
+
{ color: canContinue && image ? tokens.colors.onPrimary : tokens.colors.textSecondary },
|
|
124
|
+
]}
|
|
125
|
+
>
|
|
126
|
+
{translations.continue}
|
|
127
|
+
</AtomicText>
|
|
128
|
+
<AtomicIcon
|
|
129
|
+
name="ChevronRight"
|
|
130
|
+
size="sm"
|
|
131
|
+
color={canContinue && image ? "onPrimary" : "textSecondary"}
|
|
132
|
+
/>
|
|
133
|
+
</TouchableOpacity>
|
|
134
|
+
}
|
|
135
|
+
/>
|
|
136
|
+
<ScreenLayout
|
|
137
|
+
edges={["left", "right"]}
|
|
138
|
+
backgroundColor="transparent"
|
|
139
|
+
scrollable={true}
|
|
140
|
+
keyboardAvoiding={true}
|
|
141
|
+
contentContainerStyle={styles.scrollContent}
|
|
142
|
+
hideScrollIndicator={true}
|
|
143
|
+
>
|
|
144
|
+
<AtomicText style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
|
|
145
|
+
{translations.subtitle}
|
|
146
|
+
</AtomicText>
|
|
147
|
+
|
|
148
|
+
{/* Photo Tips - InfoGrid version */}
|
|
149
|
+
{showPhotoTips && (
|
|
150
|
+
<View style={styles.tipsContainer}>
|
|
151
|
+
<InfoGrid
|
|
152
|
+
items={photoTipsItems}
|
|
153
|
+
columns={2}
|
|
154
|
+
title={t("photoUpload.tips.title")}
|
|
155
|
+
headerIcon="Lightbulb"
|
|
156
|
+
/>
|
|
157
|
+
</View>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
<PhotoUploadCard
|
|
161
|
+
imageUri={image?.previewUrl || image?.uri || null}
|
|
162
|
+
onPress={handlePickImage}
|
|
163
|
+
isValidating={false}
|
|
164
|
+
isValid={null}
|
|
165
|
+
translations={{
|
|
166
|
+
tapToUpload: translations.tapToUpload,
|
|
167
|
+
selectPhoto: translations.selectPhoto,
|
|
168
|
+
change: translations.change,
|
|
169
|
+
analyzing: translations.analyzing,
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
{translations.aiDisclosure && (
|
|
174
|
+
<View style={styles.disclosureContainer}>
|
|
175
|
+
<AtomicText
|
|
176
|
+
type="labelSmall"
|
|
177
|
+
style={[styles.disclosureText, { color: tokens.colors.textSecondary }]}
|
|
178
|
+
>
|
|
179
|
+
{translations.aiDisclosure}
|
|
180
|
+
</AtomicText>
|
|
181
|
+
</View>
|
|
182
|
+
)}
|
|
183
|
+
</ScreenLayout>
|
|
184
|
+
</View>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const createStyles = (tokens: DesignTokens) =>
|
|
189
|
+
StyleSheet.create({
|
|
190
|
+
container: {
|
|
191
|
+
flex: 1,
|
|
192
|
+
},
|
|
193
|
+
scrollContent: {
|
|
194
|
+
paddingBottom: 40,
|
|
195
|
+
},
|
|
196
|
+
subtitle: {
|
|
197
|
+
fontSize: 16,
|
|
198
|
+
textAlign: "center",
|
|
199
|
+
marginHorizontal: 24,
|
|
200
|
+
marginBottom: 24,
|
|
201
|
+
},
|
|
202
|
+
tipsContainer: {
|
|
203
|
+
marginHorizontal: 24,
|
|
204
|
+
marginBottom: 20,
|
|
205
|
+
},
|
|
206
|
+
continueButton: {
|
|
207
|
+
flexDirection: "row",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
paddingHorizontal: tokens.spacing.md,
|
|
210
|
+
paddingVertical: tokens.spacing.xs,
|
|
211
|
+
borderRadius: tokens.borders.radius.full,
|
|
212
|
+
},
|
|
213
|
+
continueText: {
|
|
214
|
+
fontWeight: "800",
|
|
215
|
+
marginRight: 4,
|
|
216
|
+
},
|
|
217
|
+
disclosureContainer: {
|
|
218
|
+
marginTop: 24,
|
|
219
|
+
marginHorizontal: 24,
|
|
220
|
+
padding: 16,
|
|
221
|
+
borderRadius: 12,
|
|
222
|
+
backgroundColor: tokens.colors.surfaceVariant + "40",
|
|
223
|
+
},
|
|
224
|
+
disclosureText: {
|
|
225
|
+
textAlign: "center",
|
|
226
|
+
lineHeight: 18,
|
|
227
|
+
opacity: 0.8,
|
|
228
|
+
},
|
|
229
|
+
});
|