@umituz/react-native-ai-generation-content 1.23.4 → 1.24.1

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.
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Dynamic Step Builder
3
+ * Builds flow-compatible steps from wizard configurations
4
+ * Works with ANY feature - completely generic!
5
+ */
6
+
7
+ import { StepType } from "../../../../domain/entities/flow-config.types";
8
+ import type { StepDefinition } from "../../../../domain/entities/flow-config.types";
9
+ import type {
10
+ WizardFeatureConfig,
11
+ WizardStepConfig,
12
+ ScenarioBasedConfig,
13
+ buildWizardConfigFromScenario,
14
+ } from "../../domain/entities/wizard-config.types";
15
+
16
+ /**
17
+ * Convert wizard step config to flow step definition
18
+ */
19
+ const convertToFlowStep = (wizardStep: WizardStepConfig): StepDefinition => {
20
+ // Map wizard step types to flow step types
21
+ const typeMap: Record<string, StepType> = {
22
+ photo_upload: StepType.PARTNER_UPLOAD,
23
+ text_input: StepType.TEXT_INPUT,
24
+ selection: StepType.FEATURE_SELECTION,
25
+ preview: StepType.SCENARIO_PREVIEW,
26
+ };
27
+
28
+ return {
29
+ id: wizardStep.id,
30
+ type: typeMap[wizardStep.type] || StepType.CUSTOM,
31
+ required: wizardStep.required ?? true,
32
+ config: wizardStep, // Pass entire wizard config as config
33
+ };
34
+ };
35
+
36
+ /**
37
+ * Build flow steps from wizard feature config
38
+ * This is the ONLY function needed to convert any feature config to flow steps!
39
+ */
40
+ export const buildFlowStepsFromWizard = (
41
+ featureConfig: WizardFeatureConfig,
42
+ options?: {
43
+ includePreview?: boolean;
44
+ includeGenerating?: boolean;
45
+ },
46
+ ): StepDefinition[] => {
47
+ const steps: StepDefinition[] = [];
48
+
49
+ // Add scenario preview if requested
50
+ if (options?.includePreview) {
51
+ steps.push({
52
+ id: "SCENARIO_PREVIEW",
53
+ type: StepType.SCENARIO_PREVIEW,
54
+ required: true,
55
+ });
56
+ }
57
+
58
+ // Convert all wizard steps to flow steps
59
+ featureConfig.steps.forEach((wizardStep) => {
60
+ if (wizardStep.enabled !== false) {
61
+ steps.push(convertToFlowStep(wizardStep));
62
+ }
63
+ });
64
+
65
+ // Add generating step if requested
66
+ if (options?.includeGenerating) {
67
+ steps.push({
68
+ id: "GENERATING",
69
+ type: StepType.GENERATING,
70
+ required: true,
71
+ });
72
+ }
73
+
74
+ return steps;
75
+ };
76
+
77
+ /**
78
+ * Get photo upload count from wizard config
79
+ */
80
+ export const getPhotoUploadCount = (config: WizardFeatureConfig): number => {
81
+ return config.steps.filter((s) => s.type === "photo_upload").length;
82
+ };
83
+
84
+ /**
85
+ * Get step config by ID
86
+ */
87
+ export const getStepConfig = (
88
+ config: WizardFeatureConfig,
89
+ stepId: string,
90
+ ): WizardStepConfig | undefined => {
91
+ return config.steps.find((s) => s.id === stepId);
92
+ };
93
+
94
+ /**
95
+ * Quick builder for common patterns
96
+ */
97
+ export const quickBuildWizard = (
98
+ featureId: string,
99
+ scenarioConfig: ScenarioBasedConfig,
100
+ ): StepDefinition[] => {
101
+ const { buildWizardConfigFromScenario } = require("../../domain/entities/wizard-config.types");
102
+ const wizardConfig = buildWizardConfigFromScenario(featureId, scenarioConfig);
103
+ return buildFlowStepsFromWizard(wizardConfig, {
104
+ includePreview: true,
105
+ includeGenerating: true,
106
+ });
107
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Generic Step Renderer
3
+ * Renders any step type based on configuration
4
+ * NO feature-specific code! Works for ALL features!
5
+ */
6
+
7
+ import React from "react";
8
+ import { StepType } from "../../../../domain/entities/flow-config.types";
9
+ import type { StepDefinition } from "../../../../domain/entities/flow-config.types";
10
+ import type {
11
+ WizardStepConfig,
12
+ PhotoUploadStepConfig,
13
+ TextInputStepConfig,
14
+ SelectionStepConfig,
15
+ } from "../../domain/entities/wizard-config.types";
16
+
17
+ // Import generic step components (will create these)
18
+ import { PhotoUploadStep } from "../../presentation/steps/PhotoUploadStep";
19
+ import { TextInputStep } from "../../presentation/steps/TextInputStep";
20
+ import { SelectionStep } from "../../presentation/steps/SelectionStep";
21
+
22
+ export interface StepRendererProps {
23
+ readonly step: StepDefinition;
24
+ readonly onContinue: (data: Record<string, unknown>) => void;
25
+ readonly onBack: () => void;
26
+ readonly t: (key: string) => string;
27
+ readonly translations?: Record<string, string>;
28
+ }
29
+
30
+ /**
31
+ * Render a step based on its type and configuration
32
+ * This is the ONLY renderer needed for ALL features!
33
+ */
34
+ export const renderStep = (props: StepRendererProps): React.ReactElement | null => {
35
+ const { step, onContinue, onBack, t, translations } = props;
36
+
37
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
38
+ console.log("[StepRenderer] Rendering step", {
39
+ stepId: step.id,
40
+ stepType: step.type,
41
+ hasConfig: !!step.config,
42
+ });
43
+ }
44
+
45
+ const wizardConfig = step.config as WizardStepConfig | undefined;
46
+
47
+ switch (step.type) {
48
+ case StepType.PARTNER_UPLOAD: {
49
+ // This handles ALL photo uploads (couple, face-swap, image-to-video, etc.)
50
+ const photoConfig = wizardConfig as PhotoUploadStepConfig;
51
+ return (
52
+ <PhotoUploadStep
53
+ config={photoConfig}
54
+ onContinue={(imageData) => {
55
+ onContinue({ [`photo_${step.id}`]: imageData });
56
+ }}
57
+ onBack={onBack}
58
+ t={t}
59
+ translations={translations}
60
+ />
61
+ );
62
+ }
63
+
64
+ case StepType.TEXT_INPUT: {
65
+ const textConfig = wizardConfig as TextInputStepConfig;
66
+ return (
67
+ <TextInputStep
68
+ config={textConfig}
69
+ onContinue={(text) => {
70
+ onContinue({ text });
71
+ }}
72
+ onBack={onBack}
73
+ t={t}
74
+ translations={translations}
75
+ />
76
+ );
77
+ }
78
+
79
+ case StepType.FEATURE_SELECTION: {
80
+ const selectionConfig = wizardConfig as SelectionStepConfig;
81
+ return (
82
+ <SelectionStep
83
+ config={selectionConfig}
84
+ onContinue={(selected) => {
85
+ onContinue({ [`selection_${step.id}`]: selected });
86
+ }}
87
+ onBack={onBack}
88
+ t={t}
89
+ translations={translations}
90
+ />
91
+ );
92
+ }
93
+
94
+ case StepType.SCENARIO_PREVIEW:
95
+ case StepType.GENERATING:
96
+ case StepType.RESULT_PREVIEW:
97
+ // These are handled by parent wizard component
98
+ return null;
99
+
100
+ default:
101
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
102
+ console.warn("[StepRenderer] Unknown step type", step.type);
103
+ }
104
+ return null;
105
+ }
106
+ };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Generic Wizard Flow Component
3
+ * ONE wizard to rule them all!
4
+ *
5
+ * Works for:
6
+ * - Couple features (romantic-kiss, ai-hug, etc.)
7
+ * - Face swap
8
+ * - Image-to-video
9
+ * - Text-to-video
10
+ * - ANY future feature!
11
+ *
12
+ * NO feature-specific code here - everything driven by configuration!
13
+ */
14
+
15
+ import React, { useMemo, useCallback, useEffect } from "react";
16
+ import { View, StyleSheet } from "react-native";
17
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
18
+ import { useFlow } from "../../../../infrastructure/flow/useFlow";
19
+ import { StepType } from "../../../../domain/entities/flow-config.types";
20
+ import type { StepDefinition } from "../../../../domain/entities/flow-config.types";
21
+ import { renderStep } from "../../infrastructure/renderers/step-renderer";
22
+ import type { WizardFeatureConfig } from "../../domain/entities/wizard-config.types";
23
+ import { buildFlowStepsFromWizard } from "../../infrastructure/builders/dynamic-step-builder";
24
+
25
+ export interface GenericWizardFlowProps {
26
+ readonly featureConfig: WizardFeatureConfig;
27
+ readonly onStepChange?: (stepId: string, stepType: StepType | string) => void;
28
+ readonly onGenerationStart?: (data: Record<string, unknown>) => void;
29
+ readonly onBack?: () => void;
30
+ readonly t: (key: string) => string;
31
+ readonly translations?: Record<string, string>;
32
+ readonly renderPreview?: () => React.ReactElement | null;
33
+ readonly renderGenerating?: (progress: number) => React.ReactElement | null;
34
+ readonly renderResult?: (result: unknown) => React.ReactElement | null;
35
+ }
36
+
37
+ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
38
+ featureConfig,
39
+ onStepChange,
40
+ onGenerationStart,
41
+ onBack,
42
+ t,
43
+ translations,
44
+ renderPreview,
45
+ renderGenerating,
46
+ renderResult,
47
+ }) => {
48
+ const tokens = useAppDesignTokens();
49
+
50
+ // Build flow steps from wizard config
51
+ const flowSteps = useMemo<StepDefinition[]>(() => {
52
+ return buildFlowStepsFromWizard(featureConfig, {
53
+ includePreview: !!renderPreview,
54
+ includeGenerating: !!renderGenerating,
55
+ });
56
+ }, [featureConfig, renderPreview, renderGenerating]);
57
+
58
+ // Initialize flow
59
+ const flow = useFlow({
60
+ steps: flowSteps,
61
+ initialStepIndex: 0,
62
+ });
63
+
64
+ // DEBUG logging
65
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
66
+ console.log("[GenericWizardFlow] Render", {
67
+ featureId: featureConfig.id,
68
+ currentStepId: flow.currentStep?.id,
69
+ currentStepType: flow.currentStep?.type,
70
+ stepIndex: flow.currentStepIndex,
71
+ totalSteps: flowSteps.length,
72
+ });
73
+ }
74
+
75
+ // Notify parent when step changes
76
+ useEffect(() => {
77
+ if (flow.currentStep && onStepChange) {
78
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
79
+ console.log("[GenericWizardFlow] Step changed", {
80
+ stepId: flow.currentStep.id,
81
+ stepType: flow.currentStep.type,
82
+ });
83
+ }
84
+ onStepChange(flow.currentStep.id, flow.currentStep.type);
85
+ }
86
+ // eslint-disable-next-line react-hooks/exhaustive-deps
87
+ }, [flow.currentStep, flow.currentStepIndex]);
88
+
89
+ // Handle step continue
90
+ const handleStepContinue = useCallback(
91
+ (stepData: Record<string, unknown>) => {
92
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
93
+ console.log("[GenericWizardFlow] Step continue", {
94
+ stepId: flow.currentStep?.id,
95
+ data: stepData,
96
+ });
97
+ }
98
+
99
+ // Store step data in custom data
100
+ Object.entries(stepData).forEach(([key, value]) => {
101
+ flow.setCustomData(key, value);
102
+ });
103
+
104
+ // Check if this is the last step before generating
105
+ if (flow.currentStepIndex === flowSteps.length - 2) {
106
+ // Next step is GENERATING
107
+ if (onGenerationStart) {
108
+ onGenerationStart(flow.customData);
109
+ }
110
+ }
111
+
112
+ // Move to next step
113
+ flow.nextStep();
114
+ },
115
+ [flow, flowSteps.length, onGenerationStart],
116
+ );
117
+
118
+ // Handle back
119
+ const handleBack = useCallback(() => {
120
+ if (flow.currentStepIndex === 0) {
121
+ onBack?.();
122
+ } else {
123
+ flow.previousStep();
124
+ }
125
+ }, [flow, onBack]);
126
+
127
+ // Render current step
128
+ const renderCurrentStep = useCallback(() => {
129
+ const step = flow.currentStep;
130
+ if (!step) {
131
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
132
+ console.warn("[GenericWizardFlow] No current step!");
133
+ }
134
+ return null;
135
+ }
136
+
137
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
138
+ console.log("[GenericWizardFlow] Rendering step", {
139
+ stepId: step.id,
140
+ stepType: step.type,
141
+ });
142
+ }
143
+
144
+ // Special steps with custom renderers
145
+ switch (step.type) {
146
+ case StepType.SCENARIO_PREVIEW:
147
+ return renderPreview?.() || null;
148
+
149
+ case StepType.GENERATING:
150
+ return renderGenerating?.(flow.generationProgress) || null;
151
+
152
+ case StepType.RESULT_PREVIEW:
153
+ return renderResult?.(flow.generationResult) || null;
154
+
155
+ default:
156
+ // Use generic step renderer
157
+ return renderStep({
158
+ step,
159
+ onContinue: handleStepContinue,
160
+ onBack: handleBack,
161
+ t,
162
+ translations,
163
+ });
164
+ }
165
+ }, [
166
+ flow.currentStep,
167
+ flow.generationProgress,
168
+ flow.generationResult,
169
+ renderPreview,
170
+ renderGenerating,
171
+ renderResult,
172
+ handleStepContinue,
173
+ handleBack,
174
+ t,
175
+ translations,
176
+ ]);
177
+
178
+ return (
179
+ <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
180
+ {renderCurrentStep()}
181
+ </View>
182
+ );
183
+ };
184
+
185
+ const styles = StyleSheet.create({
186
+ container: {
187
+ flex: 1,
188
+ },
189
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Generic Photo Upload State Hook
3
+ * Manages photo upload state for wizard steps
4
+ * NO feature-specific logic - works for ANY photo upload
5
+ */
6
+
7
+ import { useState, useCallback } from "react";
8
+ import * as ImagePicker from "expo-image-picker";
9
+ import { Alert } from "react-native";
10
+ import type { UploadedImage } from "../../../../features/partner-upload/domain/types";
11
+
12
+ export interface PhotoUploadConfig {
13
+ readonly maxFileSizeMB?: number;
14
+ }
15
+
16
+ export interface PhotoUploadTranslations {
17
+ readonly fileTooLarge: string;
18
+ readonly maxFileSize: string;
19
+ readonly error: string;
20
+ readonly uploadFailed: string;
21
+ }
22
+
23
+ export interface UsePhotoUploadStateProps {
24
+ readonly config?: PhotoUploadConfig;
25
+ readonly translations: PhotoUploadTranslations;
26
+ }
27
+
28
+ export interface UsePhotoUploadStateReturn {
29
+ readonly image: UploadedImage | null;
30
+ readonly handlePickImage: () => Promise<void>;
31
+ readonly canContinue: boolean;
32
+ }
33
+
34
+ const DEFAULT_MAX_FILE_SIZE_MB = 10;
35
+
36
+ export const usePhotoUploadState = ({
37
+ config,
38
+ translations,
39
+ }: UsePhotoUploadStateProps): UsePhotoUploadStateReturn => {
40
+ const [image, setImage] = useState<UploadedImage | null>(null);
41
+
42
+ const maxFileSizeMB = config?.maxFileSizeMB ?? DEFAULT_MAX_FILE_SIZE_MB;
43
+
44
+ const handlePickImage = useCallback(async () => {
45
+ try {
46
+ // Request permission
47
+ const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
48
+ if (status !== "granted") {
49
+ Alert.alert(translations.error, "Permission to access media library is required");
50
+ return;
51
+ }
52
+
53
+ // Pick image
54
+ const result = await ImagePicker.launchImageLibraryAsync({
55
+ mediaTypes: "images" as any,
56
+ allowsEditing: true,
57
+ aspect: [1, 1],
58
+ quality: 0.8,
59
+ });
60
+
61
+ if (result.canceled) {
62
+ return;
63
+ }
64
+
65
+ const selectedAsset = result.assets[0];
66
+ if (!selectedAsset) {
67
+ return;
68
+ }
69
+
70
+ // Check file size
71
+ const fileSize = selectedAsset.fileSize || 0;
72
+ const fileSizeMB = fileSize / (1024 * 1024);
73
+
74
+ if (fileSizeMB > maxFileSizeMB) {
75
+ Alert.alert(
76
+ translations.fileTooLarge,
77
+ translations.maxFileSize.replace("{size}", maxFileSizeMB.toString()),
78
+ );
79
+ return;
80
+ }
81
+
82
+ // Create uploaded image object
83
+ const uploadedImage: UploadedImage = {
84
+ uri: selectedAsset.uri,
85
+ previewUrl: selectedAsset.uri,
86
+ width: selectedAsset.width,
87
+ height: selectedAsset.height,
88
+ fileSize,
89
+ };
90
+
91
+ setImage(uploadedImage);
92
+
93
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
94
+ console.log("[usePhotoUploadState] Image selected", {
95
+ width: uploadedImage.width,
96
+ height: uploadedImage.height,
97
+ fileSizeMB: fileSizeMB.toFixed(2),
98
+ });
99
+ }
100
+ } catch (error) {
101
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
102
+ console.error("[usePhotoUploadState] Error picking image", error);
103
+ }
104
+ Alert.alert(translations.error, translations.uploadFailed);
105
+ }
106
+ }, [maxFileSizeMB, translations]);
107
+
108
+ const canContinue = image !== null;
109
+
110
+ return {
111
+ image,
112
+ handlePickImage,
113
+ canContinue,
114
+ };
115
+ };